commit 19833ad38420aa5454f60b826af66359ce6d3b99 Author: Ben Melchior Date: Sun Jan 26 22:08:22 2025 +0100 first commit diff --git a/.env b/.env new file mode 100644 index 0000000..978679f --- /dev/null +++ b/.env @@ -0,0 +1,6 @@ +# SERVER +HOST=127.0.0.1 +PORT=3000 + +# DB +DATABASE_URL=postgres://hospitalapi:NVtqiNsQq2t7Lss@kn0x.tech/hospitalapi \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1bb4956 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +Cargo.lock +/target +.DS_Store \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..4ea9650 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "hospitalapi" +version = "0.1.0" +edition = "2021" + +[dependencies] +axum = "0.7.7" +tokio = { version = "1", features = ["full"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +dotenvy = "0.15.7" +sqlx = { version = "0.8.2", features = ["runtime-tokio-native-tls", "postgres"] } +chrono = "0.4.38" +chrono-tz = "0.10.0" diff --git a/README.md b/README.md new file mode 100644 index 0000000..3431956 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +## Routes + +# - / & /hospital + +List al Hospitals from Database. diff --git a/hospital.json b/hospital.json new file mode 100644 index 0000000..be8f472 --- /dev/null +++ b/hospital.json @@ -0,0 +1,16 @@ +{ + "hospitals": [ + { + "id": 1, + "name": "Centre Hospitalier", + "address": "4, rue Ernest Barblé L-1210 Luxembourg", + "phone": "+352 44 11 11" + }, + { + "id": 2, + "name": "Hopital Kirchberg", + "address": "9, rue Edward Steichen L-2540 Luxembourg", + "phone": "+352 24 68-1" + } + ] +} \ No newline at end of file diff --git a/src/db/init.rs b/src/db/init.rs new file mode 100644 index 0000000..031966b --- /dev/null +++ b/src/db/init.rs @@ -0,0 +1,131 @@ +use sqlx::PgPool; +use serde::{Deserialize, Serialize}; +use std::fs; + +#[derive(Serialize, Deserialize)] +struct HospitalData { + hospitals: Vec, +} + +#[derive(Serialize, Deserialize)] +struct Hospital { + id: i32, + name: String, + address: String, + phone: String, +} + +pub async fn initiate_database(pool: &PgPool) -> Result<(), sqlx::Error> { + // Drop the hospitals table for testing + sqlx::query("DROP TABLE IF EXISTS shifts;") + .execute(pool) + .await?; + + sqlx::query("DROP TABLE IF EXISTS hospitals;") + .execute(pool) + .await?; + + if !table_exists(pool).await? { + println!("Initializing database..."); + + println!("Creating hospitals table..."); + create_hospital_table(pool).await?; + + println!("Creating shifts table..."); + create_shifts_table(pool).await?; + + println!("Database initialization completed successfully"); + } + + Ok(()) +} + +async fn table_exists(pool: &PgPool) -> Result { + let exists = sqlx::query_scalar::<_, bool>( + r#" + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name = 'hospitals' + ) + "# + ) + .fetch_one(pool) + .await?; + + Ok(exists) +} + +async fn create_hospital_table(pool: &PgPool) -> Result<(), sqlx::Error> { + // Create the hospitals table if it doesn't exist + sqlx::query( + r#" + CREATE TABLE IF NOT EXISTS hospitals ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + address VARCHAR(255) NOT NULL, + phone VARCHAR(20) NOT NULL + ) + "#, + ) + .execute(pool) + .await?; + + // Check if hospitals table is empty before inserting + let count = sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM hospitals") + .fetch_one(pool) + .await?; + + // Only insert default hospitals if the table is empty + if count == 0 { + // Read and parse the hospital.json file + let data = fs::read_to_string("hospital.json") + .expect("Unable to read hospital.json"); + let hospital_data: HospitalData = serde_json::from_str(&data) + .expect("Unable to parse hospital.json"); + + // Insert each hospital from the JSON file + for hospital in hospital_data.hospitals { + sqlx::query( + r#" + INSERT INTO hospitals (id, name, address, phone) + VALUES ($1, $2, $3, $4) + "#, + ) + .bind(hospital.id) + .bind(hospital.name) + .bind(hospital.address) + .bind(hospital.phone) + .execute(pool) + .await?; + } + } + + Ok(()) +} + +async fn create_shifts_table(pool: &PgPool) -> Result<(), sqlx::Error> { + // Create the shifts table if it doesn't exist + sqlx::query("CREATE EXTENSION IF NOT EXISTS btree_gist") + .execute(pool) + .await?; + + sqlx::query( + r#" + CREATE TABLE IF NOT EXISTS shifts ( + id SERIAL PRIMARY KEY, + hospital_id INTEGER NOT NULL REFERENCES hospitals(id), + start_time TIMESTAMP WITH TIME ZONE NOT NULL, + end_time TIMESTAMP WITH TIME ZONE NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT no_overlapping_shifts EXCLUDE USING gist ( + tstzrange(start_time, end_time) WITH && + ) + ) + "#, + ) + .execute(pool) + .await?; + + Ok(()) +} \ No newline at end of file diff --git a/src/db/mod.rs b/src/db/mod.rs new file mode 100644 index 0000000..b0a39e9 --- /dev/null +++ b/src/db/mod.rs @@ -0,0 +1,27 @@ +use sqlx::postgres::PgPoolOptions; +use sqlx::PgPool; +use std::{env, sync::Arc}; + +mod init; +pub use init::initiate_database; + +// initialize and return an Arc wrapped PgPool instance +pub async fn create_pool() -> Arc { + // load environments variables from `.env` file + let database_url: String = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); + + // create postgres connection pool + let pool = PgPoolOptions::new() + .max_connections(5) + .connect(&database_url) + .await + .expect("Failed to create connection pool"); + + // Initialize database tables + if let Err(e) = initiate_database(&pool).await { + eprintln!("Error initializing database: {}", e); + panic!("Database initialization failed"); + } + + Arc::new(pool) +} \ No newline at end of file diff --git a/src/handlers/root.rs b/src/handlers/root.rs new file mode 100644 index 0000000..6f2f8ae --- /dev/null +++ b/src/handlers/root.rs @@ -0,0 +1,18 @@ +use axum::Json; +use serde::Serialize; + +#[derive(Serialize)] +pub struct ApiInfo { + name: &'static str, + version: &'static str, + description: &'static str, +} + +pub async fn root_handler() -> Json { + let info: ApiInfo = ApiInfo { + name: "HospitalAPI", + version: "0.1", + description: "This API provides you with the current hospital on duty in luxembourg city. Call the /getHospital endpoint.", + }; + Json(info) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..b55d55f --- /dev/null +++ b/src/main.rs @@ -0,0 +1,45 @@ +use std::{env, net::SocketAddrV4}; +use dotenvy::dotenv; +use utilities::hospital; + +mod db; +mod routes; +mod handlers { + pub mod root; +} +mod utilities { + pub mod hospital; +} + +#[tokio::main] +async fn main() { + + // load environment variables from `.env` file + dotenv().ok(); + + // get environment variables, with default if not set + let host: String = env::var("HOST").unwrap_or_else(|_| "127.0.0.1".to_string()); + let port: String = env::var("PORT").unwrap_or_else(|_| "3000".to_string()); + + // parse the addr + let addr: SocketAddrV4 = format!("{}:{}", host, port).parse::().expect("Invalid address"); + + // initialize the database pool connection + let pool: std::sync::Arc> = db::create_pool().await; + + // check if the programm was called with any arguments + let args: Vec = env::args().collect(); + if args.len() > 1 && args[1] == "create-hospital-list" { + hospital::create_hospital_list(); + return; + } + + // set up the application routes + let app: axum::Router = routes::create_router(pool); + println!("Server is running on https://{addr}"); + + // bind and serve the application + let listener: tokio::net::TcpListener = tokio::net::TcpListener::bind(&addr).await.unwrap(); + axum::serve(listener, app).await.unwrap(); + +} \ No newline at end of file diff --git a/src/routes.rs b/src/routes.rs new file mode 100644 index 0000000..f0d958f --- /dev/null +++ b/src/routes.rs @@ -0,0 +1,11 @@ +use axum::{Router, routing::get}; +use std::sync::Arc; +use sqlx::PgPool; + +use crate::handlers::root::root_handler; + +pub fn create_router(pool: Arc) -> Router { + Router::new() + .route("/", get(root_handler)) + .with_state(pool) +} \ No newline at end of file diff --git a/src/utilities/hospital.rs b/src/utilities/hospital.rs new file mode 100644 index 0000000..142e6a7 --- /dev/null +++ b/src/utilities/hospital.rs @@ -0,0 +1,177 @@ +use core::panic; +use std::io; +use chrono::{offset::LocalResult, DateTime, Datelike, Duration, Local, TimeZone}; + +struct Hospital { + id: u32, + name: String, +} + +// struct Schedule { +// from: DateTime, +// to: DateTime, +// } + +pub fn create_hospital_list() { + + // create the hospitals + let hospitals: [Hospital; 2] = [ + Hospital { + id: 1, + name: "Centre Hospitalier".to_string(), + }, + Hospital { + id: 2, + name: "Hopital Kirchberg".to_string(), + }, + ]; + + // let mut start: DateTime = Local.with_ymd_and_hms(2025, 1, 6, 8, 0, 0).unwrap(); // set date for testing + let mut start: DateTime = Local::now(); + let mut end: DateTime = get_end_of_shift(start); + + println!("It is now {} and the current Shift is ending on {}", start.format("%A %d/%m/%Y %H:%M"), end.format("%A %d/%m/%Y %H:%M")); + println!("Whitch Hospital is actualy on duty?"); + + // print out the hospitals + for hospital in &hospitals { + println!("{}) {}", hospital.id, hospital.name) + } + + let mut hospital_id_input = String::new(); + io::stdin() + .read_line(&mut hospital_id_input) + .expect("Failed to read the line"); + let hospital_id_input = hospital_id_input.trim(); + let hospital_id: i32 = match hospital_id_input.parse() { + Ok(num) => num, + Err(_) => { + println!("Please enter a valid number!"); + return; + } + }; + let mut hospital_index: usize = (hospital_id - 1) as usize; + + for _ in 1..=100 { + + // Print the current hospital on duty + println!("From {} to {} is {} on duty.", start.format("%A %d/%m/%Y %H:%M"), end.format("%A %d/%m/%Y %H:%M"), hospitals[hospital_index].name ); + + // new shift + start = end; + end = get_end_of_shift(start); + + // change hospital + hospital_index = if hospital_index == 0 { 1 } else { 0 } + } + + +} + +fn get_end_of_shift(start: DateTime) -> DateTime { + + let end: DateTime; + + // define weekday & hour + let weekday_str: String = start.format("%u").to_string(); + let weekday: u32 = weekday_str.parse().unwrap(); + let hour_str: String = start.format("%H").to_string(); + let hour: u32 = hour_str.parse().unwrap(); + + // From Monday to Thursday before 7:00 + if weekday >= 1 && weekday <= 4 && hour < 7 { + + let next_day: DateTime = start; + let year: i32 = next_day.year(); + let month: u32 = next_day.month(); + let day: u32 = next_day.day(); + + let local_result: LocalResult> = Local.with_ymd_and_hms(year, month, day, 7, 0, 0); + end = match local_result { + LocalResult::None => { + panic!("The specified date and time do not exist."); + }, + LocalResult::Single(datetime) => datetime, + LocalResult::Ambiguous(datetime1, _datetime2, ) => { + datetime1 + } + }; + + // From Monday to Wednesday after 7:00 + } else if weekday >= 1 && weekday <= 3 && hour >= 7 { + + let next_day: DateTime = start + Duration::days(1); + let year: i32 = next_day.year(); + let month: u32 = next_day.month(); + let day: u32 = next_day.day(); + + let local_result: LocalResult> = Local.with_ymd_and_hms(year, month, day, 7, 0, 0); + end = match local_result { + LocalResult::None => { + panic!("The specified date and time do not exist."); + }, + LocalResult::Single(datetime) => datetime, + LocalResult::Ambiguous(datetime1, _datetime2, ) => { + datetime1 + } + }; + + } else if weekday == 4 && hour >= 7 { + + let next_day: DateTime = start + Duration::days(1); + let year: i32 = next_day.year(); + let month: u32 = next_day.month(); + let day: u32 = next_day.day(); + + let local_result: LocalResult> = Local.with_ymd_and_hms(year, month, day, 17, 0, 0); + end = match local_result { + LocalResult::None => { + panic!("The specified date and time do not exist."); + }, + LocalResult::Single(datetime) => datetime, + LocalResult::Ambiguous(datetime1, _datetime2, ) => { + datetime1 + } + }; + + } else if weekday == 5 && hour < 17 { + + let next_day: DateTime = start; + let year: i32 = next_day.year(); + let month: u32 = next_day.month(); + let day: u32 = next_day.day(); + + let local_result: LocalResult> = Local.with_ymd_and_hms(year, month, day, 17, 0, 0); + end = match local_result { + LocalResult::None => { + panic!("The specified date and time do not exist."); + }, + LocalResult::Single(datetime) => datetime, + LocalResult::Ambiguous(datetime1, _datetime2, ) => { + datetime1 + } + }; + + // Friday after 17:00 + } else { + + let count_days: i64 = 8 - weekday as i64; + let next_day: DateTime = start + Duration::days(count_days); + let year: i32 = next_day.year(); + let month: u32 = next_day.month(); + let day: u32 = next_day.day(); + + let local_result: LocalResult> = Local.with_ymd_and_hms(year, month, day, 7, 0, 0); + end = match local_result { + LocalResult::None => { + panic!("The specified date and time do not exist."); + }, + LocalResult::Single(datetime) => datetime, + LocalResult::Ambiguous(datetime1, _datetime2, ) => { + datetime1 + } + }; + } + + end +} \ No newline at end of file