feat: initial base implementation

This commit is contained in:
Timo Ley 2023-06-21 10:09:28 +02:00
parent b3e1badef2
commit 92799df15e
8 changed files with 216 additions and 5 deletions

3
.gitignore vendored
View File

@ -1 +1,4 @@
/target
config.toml
test.sql
Cargo.lock

View File

@ -1,5 +1,5 @@
[package]
name = "MS3"
name = "ms3"
version = "0.1.0"
edition = "2021"
@ -7,4 +7,13 @@ edition = "2021"
[dependencies]
tokio = { version = "1.0", features = ["full"] }
sibyl = "0.6.16"
sibyl = { version = "0.6", features = ["nonblocking", "tokio"] }
axum = { version = "0.2.8", features = ["headers", "multipart"] }
hyper = "0.14.16"
tower = { version = "0.4", features = ["util", "timeout"] }
tower-http = { version = "0.1", features = ["add-extension", "trace", "fs", "set-header"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.51"
structopt = "0.3.22"
toml = "0.5.8"
thiserror = "1.0.30"

11
src/config.rs Normal file
View File

@ -0,0 +1,11 @@
use std::net::SocketAddr;
use serde::Deserialize;
#[derive(Deserialize)]
pub struct Config {
pub addr: SocketAddr,
pub db_name: String,
pub db_username: String,
pub db_password: String,
}

65
src/error.rs Normal file
View File

@ -0,0 +1,65 @@
use std::convert::Infallible;
use axum::{response::IntoResponse, body::{Full, Bytes}, Json};
use hyper::StatusCode;
use serde::{Serializer, Serialize};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ApplicationError {
#[error("File read error: {0}")]
Read(#[from] std::io::Error),
#[error("Deserialize error: {0}")]
Deserialize(#[from] toml::de::Error),
#[error("Database connection error: {0}")]
Database(#[from] sibyl::Error),
#[error("Axum error: {0}")]
Axum(#[from] hyper::Error),
}
#[derive(Error, Debug)]
pub enum ServiceError {
#[error("SQL error: {0}")]
Database(#[from] sibyl::Error),
#[error("Response code: {0}")]
ErrorResponse(StatusCode, Option<String>),
}
fn serialize_status<S>(x: &StatusCode, s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
s.serialize_u16(x.as_u16())
}
#[derive(Serialize)]
pub struct ErrorResponse {
#[serde(serialize_with = "serialize_status")]
pub status: StatusCode,
pub error: String,
}
impl IntoResponse for ServiceError {
type Body = Full<Bytes>;
type BodyError = Infallible;
fn into_response(self) -> axum::http::Response<Self::Body> {
let res = match self {
ServiceError::Database(err) => ErrorResponse::new(StatusCode::INTERNAL_SERVER_ERROR, Some(err.to_string())),
ServiceError::ErrorResponse(code, reason) => ErrorResponse::new(code, reason),
};
let status = res.status;
(status, Json(res)).into_response()
}
}
impl ErrorResponse {
fn new(status: StatusCode, message: Option<String>) -> Self {
let reason = status.canonical_reason().unwrap_or_default();
Self {
status,
error: message.unwrap_or_else(|| reason.to_string()),
}
}
}

View File

@ -1,5 +1,61 @@
#[tokio::main]
use std::{path::PathBuf, sync::Arc};
async fn main() {
println!("Hello, world!");
use axum::{Router, AddExtensionLayer, http::{HeaderValue, header}};
use error::ApplicationError;
use hyper::{Request, Body};
use sibyl::{Environment, SessionPool};
use structopt::{StructOpt, lazy_static::lazy_static};
use config::Config;
use tower_http::set_header::SetResponseHeaderLayer;
mod config;
mod error;
mod routes;
mod sql;
mod model;
#[derive(StructOpt)]
struct Opt {
#[structopt(
short,
long,
help = "config file to use",
default_value = "./config.toml"
)]
config: PathBuf,
}
pub struct ServiceInner {
pool: SessionPool<'static>
}
pub type Service = Arc<ServiceInner>;
lazy_static!{
pub static ref ORACLE: Environment = sibyl::env().expect("Sibyl error");
}
#[tokio::main]
async fn main() -> Result<(), ApplicationError> {
let opt = Opt::from_args();
let config = std::fs::read(&opt.config)?;
let config = toml::from_slice::<Config>(&config)?;
let pool = ORACLE.create_session_pool(&config.db_name, &config.db_username, &config.db_password, 0, 1, 10).await?;
let service: Service = Arc::new(ServiceInner{ pool });
let app = Router::new()
.nest("/api", routes::routes())
.layer(AddExtensionLayer::new(service))
.layer(SetResponseHeaderLayer::<_, Request<Body>>::if_not_present(
header::ACCESS_CONTROL_ALLOW_ORIGIN,
HeaderValue::from_static("*"),
));
axum::Server::bind(&config.addr)
.serve(app.into_make_service())
.await?;
Ok(())
}

11
src/model.rs Normal file
View File

@ -0,0 +1,11 @@
use serde::Serialize;
#[derive(Serialize)]
pub struct Room {
pub room_number: i32,
pub floor: i32,
pub size: i32,
pub room_type: String,
pub beds: i32,
pub accessibility: bool,
}

17
src/routes.rs Normal file
View File

@ -0,0 +1,17 @@
use axum::{Router, routing::BoxRoute, extract::Extension, response::IntoResponse, Json, handler::get};
use crate::{Service, error::ServiceError};
async fn rooms(
Extension(service): Extension<Service>,
) -> Result<impl IntoResponse, ServiceError> {
let rooms = service.get_rooms().await?;
Ok(Json(rooms))
}
pub fn routes() -> Router<BoxRoute> {
Router::new()
.route("/rooms", get(rooms))
.boxed()
}

39
src/sql.rs Normal file
View File

@ -0,0 +1,39 @@
use crate::{ServiceInner, model::Room, error::ServiceError};
impl ServiceInner {
pub async fn get_rooms(&self) -> Result<Vec<Room>, ServiceError> {
let session = self.pool.get_session().await?;
let stmt = session.prepare("
SELECT
roomNumber,
floor,
roomTyp,
\"size\",
accessibility,
beds
FROM room
").await?;
let rows = stmt.query("").await?;
let mut rooms: Vec<Room> = vec![];
while let Some(row) = rows.next().await? {
let acc: i32 = row.get(5)?;
let room = Room {
room_number: row.get(0)?,
floor: row.get(1)?,
size: row.get(3)?,
room_type: row.get(2)?,
beds: row.get(4)?,
accessibility: acc != 0,
};
rooms.push(room);
}
Ok(rooms)
}
}