From ba78627169ed88359c083e83c81c733a9f4984c8 Mon Sep 17 00:00:00 2001 From: Ben Melchior Date: Sat, 12 Apr 2025 21:52:34 +0200 Subject: [PATCH] Enhance application configuration and functionality - Updated `.env` file to include `DB_MAX_CONNECTIONS` and `ENVIRONMENT` variables. - Modified `Cargo.toml` to add `thiserror` and `time` dependencies, and updated `sqlx` features. - Refactored `main.rs` to implement periodic tasks for checking future shifts and printing the current hospital. - Expanded routing in `routes.rs` to include a new endpoint for current hospital status. - Improved database initialization in `init.rs` to conditionally drop tables based on the environment. - Enhanced error handling in database operations and added logging for better traceability. - Updated hospital management logic in `hospital.rs` to fetch and store shifts with improved timestamp logging. --- .env | 5 +- Cargo.toml | 4 +- src/db/init.rs | 51 ++++-- src/db/mod.rs | 34 +++- src/handlers/current_hospital.rs | 79 +++++++++ src/handlers/mod.rs | 2 + src/main.rs | 56 ++++-- src/routes.rs | 2 + src/utilities/hospital.rs | 289 ++++++++++++++++++++++++++++--- 9 files changed, 455 insertions(+), 67 deletions(-) create mode 100644 src/handlers/current_hospital.rs create mode 100644 src/handlers/mod.rs diff --git a/.env b/.env index 978679f..7126398 100644 --- a/.env +++ b/.env @@ -3,4 +3,7 @@ HOST=127.0.0.1 PORT=3000 # DB -DATABASE_URL=postgres://hospitalapi:NVtqiNsQq2t7Lss@kn0x.tech/hospitalapi \ No newline at end of file +DATABASE_URL=postgres://hospitalapi:NVtqiNsQq2t7Lss@kn0x.tech/hospitalapi +DB_MAX_CONNECTIONS=5 + +ENVIRONMENT=production \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 4ea9650..15759dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,8 @@ 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"] } +sqlx = { version = "0.8.2", features = ["runtime-tokio-native-tls", "postgres", "time"] } chrono = "0.4.38" chrono-tz = "0.10.0" +thiserror = "1.0" +time = "0.3" diff --git a/src/db/init.rs b/src/db/init.rs index 031966b..803b265 100644 --- a/src/db/init.rs +++ b/src/db/init.rs @@ -1,6 +1,18 @@ use sqlx::PgPool; use serde::{Deserialize, Serialize}; -use std::fs; +use std::{fs, env}; +use thiserror::Error; +use crate::utilities::hospital; + +#[derive(Error, Debug)] +pub enum InitError { + #[error("Database error: {0}")] + Database(#[from] sqlx::Error), + #[error("File error: {0}")] + File(#[from] std::io::Error), + #[error("JSON parsing error: {0}")] + Json(#[from] serde_json::Error), +} #[derive(Serialize, Deserialize)] struct HospitalData { @@ -15,15 +27,17 @@ struct Hospital { 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?; +pub async fn initiate_database(pool: &PgPool) -> Result<(), InitError> { + // Only drop tables in development environment + if env::var("ENVIRONMENT").unwrap_or_else(|_| "development".to_string()) == "development" { + sqlx::query("DROP TABLE IF EXISTS shifts;") + .execute(pool) + .await?; - sqlx::query("DROP TABLE IF EXISTS hospitals;") - .execute(pool) - .await?; + sqlx::query("DROP TABLE IF EXISTS hospitals;") + .execute(pool) + .await?; + } if !table_exists(pool).await? { println!("Initializing database..."); @@ -35,6 +49,10 @@ pub async fn initiate_database(pool: &PgPool) -> Result<(), sqlx::Error> { create_shifts_table(pool).await?; println!("Database initialization completed successfully"); + + println!("Generating hospital schedule..."); + let pool_arc = std::sync::Arc::new(pool.clone()); + hospital::create_hospital_list(pool_arc).await.map_err(|e| InitError::Database(e))?; } Ok(()) @@ -56,7 +74,7 @@ async fn table_exists(pool: &PgPool) -> Result { Ok(exists) } -async fn create_hospital_table(pool: &PgPool) -> Result<(), sqlx::Error> { +async fn create_hospital_table(pool: &PgPool) -> Result<(), InitError> { // Create the hospitals table if it doesn't exist sqlx::query( r#" @@ -64,7 +82,9 @@ async fn create_hospital_table(pool: &PgPool) -> Result<(), sqlx::Error> { id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, address VARCHAR(255) NOT NULL, - phone VARCHAR(20) NOT NULL + phone VARCHAR(20) NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ) "#, ) @@ -79,10 +99,8 @@ async fn create_hospital_table(pool: &PgPool) -> Result<(), sqlx::Error> { // 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"); + let data = fs::read_to_string("hospital.json")?; + let hospital_data: HospitalData = serde_json::from_str(&data)?; // Insert each hospital from the JSON file for hospital in hospital_data.hospitals { @@ -104,7 +122,7 @@ async fn create_hospital_table(pool: &PgPool) -> Result<(), sqlx::Error> { Ok(()) } -async fn create_shifts_table(pool: &PgPool) -> Result<(), sqlx::Error> { +async fn create_shifts_table(pool: &PgPool) -> Result<(), InitError> { // Create the shifts table if it doesn't exist sqlx::query("CREATE EXTENSION IF NOT EXISTS btree_gist") .execute(pool) @@ -117,7 +135,6 @@ async fn create_shifts_table(pool: &PgPool) -> Result<(), sqlx::Error> { 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 && ) diff --git a/src/db/mod.rs b/src/db/mod.rs index b0a39e9..b754efd 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,27 +1,43 @@ use sqlx::postgres::PgPoolOptions; use sqlx::PgPool; use std::{env, sync::Arc}; +use thiserror::Error; mod init; pub use init::initiate_database; +#[derive(Error, Debug)] +pub enum DatabaseError { + #[error("Database URL not found in environment")] + MissingDatabaseUrl, + #[error("Failed to create connection pool: {0}")] + PoolCreationError(#[from] sqlx::Error), + #[error("Database initialization failed: {0}")] + InitializationError(String), +} + // initialize and return an Arc wrapped PgPool instance -pub async fn create_pool() -> Arc { +pub async fn create_pool() -> Result, DatabaseError> { // load environments variables from `.env` file - let database_url: String = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); + let database_url = env::var("DATABASE_URL").map_err(|_| DatabaseError::MissingDatabaseUrl)?; + + // Get configuration from environment or use defaults + let max_connections = env::var("DB_MAX_CONNECTIONS") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(5); // create postgres connection pool let pool = PgPoolOptions::new() - .max_connections(5) + .max_connections(max_connections) .connect(&database_url) .await - .expect("Failed to create connection pool"); + .map_err(DatabaseError::PoolCreationError)?; // Initialize database tables - if let Err(e) = initiate_database(&pool).await { - eprintln!("Error initializing database: {}", e); - panic!("Database initialization failed"); - } + initiate_database(&pool) + .await + .map_err(|e| DatabaseError::InitializationError(e.to_string()))?; - Arc::new(pool) + Ok(Arc::new(pool)) } \ No newline at end of file diff --git a/src/handlers/current_hospital.rs b/src/handlers/current_hospital.rs new file mode 100644 index 0000000..509f3e6 --- /dev/null +++ b/src/handlers/current_hospital.rs @@ -0,0 +1,79 @@ +use axum::{ + extract::State, + Json, +}; +use serde::Serialize; +use std::sync::Arc; +use sqlx::PgPool; +use chrono::Local; +use time::OffsetDateTime; + +#[derive(Serialize)] +pub struct CurrentHospital { + id: i32, + name: String, + start_time: String, + end_time: String, +} + +#[derive(Serialize)] +pub struct HospitalResponse { + success: bool, + hospital: Option, + message: String, +} + +pub async fn current_hospital_handler( + State(pool): State>, +) -> Json { + // Get the current time + let now = Local::now(); + + // Convert chrono DateTime to time OffsetDateTime for SQLx + let now_offset = OffsetDateTime::from_unix_timestamp(now.timestamp()) + .expect("Failed to convert to OffsetDateTime"); + + // Get the current hospital on duty + let result = sqlx::query!( + r#" + SELECT h.id, h.name, s.start_time, s.end_time + FROM hospitals h + JOIN shifts s ON h.id = s.hospital_id + WHERE s.start_time <= $1 AND s.end_time > $1 + ORDER BY s.end_time ASC + LIMIT 1 + "#, + now_offset + ) + .fetch_optional(&*pool) + .await; + + match result { + Ok(Some(hospital)) => { + Json(HospitalResponse { + success: true, + hospital: Some(CurrentHospital { + id: hospital.id, + name: hospital.name, + start_time: hospital.start_time.to_string(), + end_time: hospital.end_time.to_string(), + }), + message: "Current hospital retrieved successfully".to_string(), + }) + }, + Ok(None) => { + Json(HospitalResponse { + success: false, + hospital: None, + message: "No hospital currently on duty".to_string(), + }) + }, + Err(e) => { + Json(HospitalResponse { + success: false, + hospital: None, + message: format!("Error retrieving current hospital: {}", e), + }) + } + } +} \ No newline at end of file diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs new file mode 100644 index 0000000..8732ae8 --- /dev/null +++ b/src/handlers/mod.rs @@ -0,0 +1,2 @@ +pub mod root; +pub mod current_hospital; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index b55d55f..0f18444 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,24 @@ -use std::{env, net::SocketAddrV4}; +use std::{env, net::SocketAddrV4, time::Duration as StdDuration}; use dotenvy::dotenv; use utilities::hospital; +use tokio::time; +use chrono::Local; mod db; mod routes; -mod handlers { - pub mod root; -} +mod handlers; mod utilities { pub mod hospital; } +// Helper function to print with timestamp +fn log_with_timestamp(message: &str) { + let now = Local::now(); + println!("[{}] {}", now.format("%Y-%m-%d %H:%M:%S"), message); +} + #[tokio::main] async fn main() { - // load environment variables from `.env` file dotenv().ok(); @@ -25,21 +30,42 @@ async fn main() { 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; - } + let pool: std::sync::Arc> = db::create_pool() + .await + .expect("Failed to create database pool"); + + // Set up a periodic task to check for future shifts every 48 hours + let pool_clone = pool.clone(); + tokio::spawn(async move { + let mut interval = time::interval(StdDuration::from_secs(48 * 60 * 60)); // 48 hours + loop { + interval.tick().await; + log_with_timestamp("Periodic check for future shifts..."); + if let Err(e) = hospital::check_and_create_future_shifts(pool_clone.clone()).await { + eprintln!("[{}] Error checking for future shifts: {}", + Local::now().format("%Y-%m-%d %H:%M:%S"), e); + } + } + }); + + // Set up a task to print the current hospital every minute + let pool_clone = pool.clone(); + tokio::spawn(async move { + let mut interval = time::interval(StdDuration::from_secs(60)); // 1 minute + loop { + interval.tick().await; + if let Err(e) = hospital::print_current_hospital(pool_clone.clone()).await { + eprintln!("[{}] Error printing current hospital: {}", + Local::now().format("%Y-%m-%d %H:%M:%S"), e); + } + } + }); // set up the application routes let app: axum::Router = routes::create_router(pool); - println!("Server is running on https://{addr}"); + log_with_timestamp(&format!("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 index f0d958f..d235266 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -3,9 +3,11 @@ use std::sync::Arc; use sqlx::PgPool; use crate::handlers::root::root_handler; +use crate::handlers::current_hospital::current_hospital_handler; pub fn create_router(pool: Arc) -> Router { Router::new() .route("/", get(root_handler)) + .route("/current-hospital", get(current_hospital_handler)) .with_state(pool) } \ No newline at end of file diff --git a/src/utilities/hospital.rs b/src/utilities/hospital.rs index 142e6a7..a9a4da8 100644 --- a/src/utilities/hospital.rs +++ b/src/utilities/hospital.rs @@ -1,9 +1,14 @@ use core::panic; use std::io; use chrono::{offset::LocalResult, DateTime, Datelike, Duration, Local, TimeZone}; +use sqlx::PgPool; +use std::sync::Arc; +use time::OffsetDateTime; +use std::sync::Mutex; +#[derive(Debug)] struct Hospital { - id: u32, + id: i32, name: String, } @@ -12,30 +17,43 @@ struct Hospital { // to: DateTime, // } -pub fn create_hospital_list() { +// Helper function to print with timestamp +fn log_with_timestamp(message: &str) { + let now = Local::now(); + println!("[{}] {}", now.format("%Y-%m-%d %H:%M:%S"), message); +} - // create the hospitals - let hospitals: [Hospital; 2] = [ - Hospital { - id: 1, - name: "Centre Hospitalier".to_string(), - }, - Hospital { - id: 2, - name: "Hopital Kirchberg".to_string(), - }, - ]; +// Static variable to store the last printed hospital ID +static LAST_HOSPITAL_ID: Mutex> = Mutex::new(None); + +pub async fn create_hospital_list(pool: Arc) -> Result<(), sqlx::Error> { + // Fetch hospitals from the database + let hospitals = sqlx::query_as!( + Hospital, + r#" + SELECT id, name FROM hospitals ORDER BY id + "# + ) + .fetch_all(&*pool) + .await?; + + if hospitals.is_empty() { + log_with_timestamp("No hospitals found in the database."); + return Ok(()); + } // 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?"); + log_with_timestamp(&format!("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"))); + log_with_timestamp("Which Hospital is currently on duty?"); // print out the hospitals for hospital in &hospitals { - println!("{}) {}", hospital.id, hospital.name) + log_with_timestamp(&format!("{}) {}", hospital.id, hospital.name)); } let mut hospital_id_input = String::new(); @@ -46,26 +64,249 @@ pub fn create_hospital_list() { let hospital_id: i32 = match hospital_id_input.parse() { Ok(num) => num, Err(_) => { - println!("Please enter a valid number!"); - return; + log_with_timestamp("Please enter a valid number!"); + return Ok(()); } }; - let mut hospital_index: usize = (hospital_id - 1) as usize; + + // Find the index of the selected hospital + let mut hospital_index: usize = 0; + let mut found = false; + for (i, hospital) in hospitals.iter().enumerate() { + if hospital.id == hospital_id { + hospital_index = i; + found = true; + break; + } + } + + if !found { + log_with_timestamp(&format!("Hospital with ID {} not found.", hospital_id)); + return Ok(()); + } - 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 ); + log_with_timestamp("Generating and storing shifts in the database..."); + + // Clear existing shifts + sqlx::query("DELETE FROM shifts") + .execute(&*pool) + .await?; + + // Generate and store 100 shifts + for i in 1..=100 { + // Format the timestamps as strings for database storage + let start_str = start.format("%Y-%m-%d %H:%M:%S").to_string(); + let end_str = end.format("%Y-%m-%d %H:%M:%S").to_string(); + + // Store the current shift in the database + sqlx::query( + r#" + INSERT INTO shifts (hospital_id, start_time, end_time) + VALUES ($1, $2::timestamp with time zone, $3::timestamp with time zone) + "#, + ) + .bind(hospitals[hospital_index].id) + .bind(start_str) + .bind(end_str) + .execute(&*pool) + .await?; + + // Print progress only every 10 shifts + if i % 10 == 0 { + log_with_timestamp(&format!("Stored {} shifts...", i)); + } // new shift start = end; end = get_end_of_shift(start); // change hospital - hospital_index = if hospital_index == 0 { 1 } else { 0 } + hospital_index = if hospital_index == 0 { 1 } else { 0 }; + + // If we only have one hospital, don't switch + if hospitals.len() == 1 { + hospital_index = 0; + } } + + log_with_timestamp("Successfully stored 100 shifts in the database."); + Ok(()) +} +pub async fn check_and_create_future_shifts(pool: Arc) -> Result<(), sqlx::Error> { + // Get the current time + let now = Local::now(); + + // Calculate the date two months from now + let two_months_from_now = now + Duration::days(60); + + // Convert chrono DateTime to time OffsetDateTime for SQLx + let now_offset = OffsetDateTime::from_unix_timestamp(now.timestamp()) + .expect("Failed to convert to OffsetDateTime"); + let two_months_offset = OffsetDateTime::from_unix_timestamp(two_months_from_now.timestamp()) + .expect("Failed to convert to OffsetDateTime"); + + // Check if there are any shifts in the next two months + let future_shifts_count = sqlx::query_scalar::<_, i64>( + r#" + SELECT COUNT(*) FROM shifts + WHERE start_time > $1 AND start_time < $2 + "#, + ) + .bind(now_offset) + .bind(two_months_offset) + .fetch_one(&*pool) + .await?; + + // If there are no future shifts, create them + if future_shifts_count == 0 { + log_with_timestamp("No future shifts found. Creating shifts for the next two months..."); + + // Get the current hospital on duty + let current_hospital = sqlx::query_as!( + Hospital, + r#" + SELECT h.id, h.name + FROM hospitals h + JOIN shifts s ON h.id = s.hospital_id + WHERE s.end_time > $1 + ORDER BY s.end_time ASC + LIMIT 1 + "#, + now_offset + ) + .fetch_optional(&*pool) + .await?; + + // If no current hospital is found, get the first hospital from the database + let hospital_id = match current_hospital { + Some(hospital) => hospital.id, + None => { + let first_hospital = sqlx::query_as!( + Hospital, + r#" + SELECT id, name FROM hospitals ORDER BY id LIMIT 1 + "# + ) + .fetch_optional(&*pool) + .await? + .ok_or_else(|| sqlx::Error::RowNotFound)?; + + first_hospital.id + } + }; + + // Find the index of the selected hospital + let hospitals = sqlx::query_as!( + Hospital, + r#" + SELECT id, name FROM hospitals ORDER BY id + "# + ) + .fetch_all(&*pool) + .await?; + + let mut hospital_index = 0; + for (i, hospital) in hospitals.iter().enumerate() { + if hospital.id == hospital_id { + hospital_index = i; + break; + } + } + + // Generate shifts starting from now + let mut start = now; + let mut end = get_end_of_shift(start); + + // Generate shifts for the next two months (approximately 60 days) + for _ in 1..=120 { + // Convert chrono DateTime to time OffsetDateTime for SQLx + let start_offset = OffsetDateTime::from_unix_timestamp(start.timestamp()) + .expect("Failed to convert to OffsetDateTime"); + let end_offset = OffsetDateTime::from_unix_timestamp(end.timestamp()) + .expect("Failed to convert to OffsetDateTime"); + + // Store the current shift in the database + sqlx::query( + r#" + INSERT INTO shifts (hospital_id, start_time, end_time) + VALUES ($1, $2, $3) + "#, + ) + .bind(hospitals[hospital_index].id) + .bind(start_offset) + .bind(end_offset) + .execute(&*pool) + .await?; + + // new shift + start = end; + end = get_end_of_shift(start); + + // change hospital + hospital_index = if hospital_index == 0 { 1 } else { 0 }; + + // If we only have one hospital, don't switch + if hospitals.len() == 1 { + hospital_index = 0; + } + } + + log_with_timestamp("Successfully created future shifts for the next two months."); + } else { + log_with_timestamp(&format!("Found {} future shifts. No need to create more.", future_shifts_count)); + } + + Ok(()) +} +pub async fn print_current_hospital(pool: Arc) -> Result<(), sqlx::Error> { + // Get the current time + let now = Local::now(); + + // Convert chrono DateTime to time OffsetDateTime for SQLx + let now_offset = OffsetDateTime::from_unix_timestamp(now.timestamp()) + .expect("Failed to convert to OffsetDateTime"); + + // Get the current hospital on duty + let current_hospital = sqlx::query_as!( + Hospital, + r#" + SELECT h.id, h.name + FROM hospitals h + JOIN shifts s ON h.id = s.hospital_id + WHERE s.start_time <= $1 AND s.end_time > $1 + ORDER BY s.end_time ASC + LIMIT 1 + "#, + now_offset + ) + .fetch_optional(&*pool) + .await?; + + // Get the last printed hospital ID + let mut last_id = LAST_HOSPITAL_ID.lock().unwrap(); + + // Check if the hospital has changed + let current_id = current_hospital.as_ref().map(|h| h.id); + let has_changed = *last_id != current_id; + + // Only print if the hospital has changed + if has_changed { + match ¤t_hospital { + Some(hospital) => { + log_with_timestamp(&format!("Current hospital on duty: {} (ID: {})", hospital.name, hospital.id)); + }, + None => { + log_with_timestamp("No hospital currently on duty."); + } + } + + // Update the last printed hospital ID + *last_id = current_id; + } + + Ok(()) } fn get_end_of_shift(start: DateTime) -> DateTime {