From 92799df15e58345c373c2a8b90eaf994f037f7df Mon Sep 17 00:00:00 2001 From: Timo Ley Date: Wed, 21 Jun 2023 10:09:28 +0200 Subject: [PATCH] feat: initial base implementation --- .gitignore | 3 +++ Cargo.toml | 13 +++++++++-- src/config.rs | 11 +++++++++ src/error.rs | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 62 +++++++++++++++++++++++++++++++++++++++++++++--- src/model.rs | 11 +++++++++ src/routes.rs | 17 ++++++++++++++ src/sql.rs | 39 +++++++++++++++++++++++++++++++ 8 files changed, 216 insertions(+), 5 deletions(-) create mode 100644 src/config.rs create mode 100644 src/error.rs create mode 100644 src/model.rs create mode 100644 src/routes.rs create mode 100644 src/sql.rs diff --git a/.gitignore b/.gitignore index ea8c4bf..5ac7bec 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ /target +config.toml +test.sql +Cargo.lock \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index eb096aa..fee0f73 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" \ No newline at end of file diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..400447a --- /dev/null +++ b/src/config.rs @@ -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, +} \ No newline at end of file diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..214615e --- /dev/null +++ b/src/error.rs @@ -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), +} + +fn serialize_status(x: &StatusCode, s: S) -> Result +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; + + type BodyError = Infallible; + + fn into_response(self) -> axum::http::Response { + 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) -> Self { + let reason = status.canonical_reason().unwrap_or_default(); + Self { + status, + error: message.unwrap_or_else(|| reason.to_string()), + } + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 64775fd..1b18c66 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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; + +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)?; + + 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>::if_not_present( + header::ACCESS_CONTROL_ALLOW_ORIGIN, + HeaderValue::from_static("*"), + )); + + axum::Server::bind(&config.addr) + .serve(app.into_make_service()) + .await?; + + Ok(()) +} \ No newline at end of file diff --git a/src/model.rs b/src/model.rs new file mode 100644 index 0000000..621ec28 --- /dev/null +++ b/src/model.rs @@ -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, +} \ No newline at end of file diff --git a/src/routes.rs b/src/routes.rs new file mode 100644 index 0000000..b220758 --- /dev/null +++ b/src/routes.rs @@ -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, +) -> Result { + let rooms = service.get_rooms().await?; + Ok(Json(rooms)) +} + + +pub fn routes() -> Router { + Router::new() + .route("/rooms", get(rooms)) + .boxed() +} \ No newline at end of file diff --git a/src/sql.rs b/src/sql.rs new file mode 100644 index 0000000..307d501 --- /dev/null +++ b/src/sql.rs @@ -0,0 +1,39 @@ +use crate::{ServiceInner, model::Room, error::ServiceError}; + +impl ServiceInner { + + pub async fn get_rooms(&self) -> Result, 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 = 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) + } + +} \ No newline at end of file