use std::{net::SocketAddr, sync::Arc}; use axum::Router; use serde::{Deserialize, Serialize}; use tokio::net::TcpListener; use unnamed_server::{state::AppState, Error}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Config { listen_addr: Option, jwt_secret: Option, database_url: Option, } impl Config { pub fn new() -> Self { Self::default() } pub fn file>(self, file: P) -> Result { match std::fs::read_to_string(file) { Ok(s) => Ok(self.merge(toml::from_str(&s)?)), Err(err) => { tracing::warn!("Error reading config file: {err}"); Ok(self) } } } pub fn env(self, prefix: &str) -> Result { Ok(self.merge(Self { listen_addr: std::env::var(format!("{prefix}LISTEN_ADDR")) .ok() .and_then(|v| v.parse().ok()), jwt_secret: std::env::var(format!("{prefix}JWT_SECRET")).ok(), database_url: std::env::var(format!("{prefix}DATABASE_URL")).ok(), })) } pub async fn build(self) -> Result<(TcpListener, Router), Error> { macro_rules! try_extract { ($($i:ident),+) => { $(let Some($i) = self.$i else { return Err(Error::Config(format!("Missing value: {}", stringify!($i)))) };)+ }; } try_extract!(listen_addr, jwt_secret, database_url); let listener = TcpListener::bind(listen_addr).await?; let pool = init_db(&database_url).await?; let app_state = Arc::new(AppState { pool, jwt_secret }); let app = unnamed_server::init_router(app_state); Ok((listener, app)) } /// Merge self with other, overwriting any existing values on self with other's. fn merge(self, other: Self) -> Self { Self { listen_addr: other.listen_addr.or(self.listen_addr), jwt_secret: other.jwt_secret.or(self.jwt_secret), database_url: other.database_url.or(self.database_url), } } } impl Default for Config { fn default() -> Self { Self { listen_addr: Some(SocketAddr::from(([127, 0, 0, 1], 30000))), jwt_secret: None, database_url: None, } } } async fn init_db(uri: &str) -> Result, Error> { let pool = sqlx::postgres::PgPoolOptions::new() .max_connections(10) .connect(uri) .await?; sqlx::migrate!().run(&pool).await?; Ok(pool) }