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.
This commit is contained in:
Ben Melchior
2025-04-12 21:52:34 +02:00
parent 19833ad384
commit ba78627169
9 changed files with 455 additions and 67 deletions

5
.env
View File

@@ -3,4 +3,7 @@ HOST=127.0.0.1
PORT=3000
# DB
DATABASE_URL=postgres://hospitalapi:NVtqiNsQq2t7Lss@kn0x.tech/hospitalapi
DATABASE_URL=postgres://hospitalapi:NVtqiNsQq2t7Lss@kn0x.tech/hospitalapi
DB_MAX_CONNECTIONS=5
ENVIRONMENT=production

View File

@@ -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"

View File

@@ -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<bool, sqlx::Error> {
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 &&
)

View File

@@ -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<PgPool> {
pub async fn create_pool() -> Result<Arc<PgPool>, 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))
}

View File

@@ -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<CurrentHospital>,
message: String,
}
pub async fn current_hospital_handler(
State(pool): State<Arc<PgPool>>,
) -> Json<HospitalResponse> {
// 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),
})
}
}
}

2
src/handlers/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod root;
pub mod current_hospital;

View File

@@ -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::<SocketAddrV4>().expect("Invalid address");
// initialize the database pool connection
let pool: std::sync::Arc<sqlx::Pool<sqlx::Postgres>> = db::create_pool().await;
// check if the programm was called with any arguments
let args: Vec<String> = env::args().collect();
if args.len() > 1 && args[1] == "create-hospital-list" {
hospital::create_hospital_list();
return;
}
let pool: std::sync::Arc<sqlx::Pool<sqlx::Postgres>> = 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();
}

View File

@@ -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<PgPool>) -> Router {
Router::new()
.route("/", get(root_handler))
.route("/current-hospital", get(current_hospital_handler))
.with_state(pool)
}

View File

@@ -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<chrono_tz::Tz>,
// }
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<Option<i32>> = Mutex::new(None);
pub async fn create_hospital_list(pool: Arc<PgPool>) -> 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> = Local.with_ymd_and_hms(2025, 1, 6, 8, 0, 0).unwrap(); // set date for testing
let mut start: DateTime<Local> = Local::now();
let mut end: DateTime<Local> = 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<PgPool>) -> 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<PgPool>) -> 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 &current_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<Local>) -> DateTime<Local> {