mirror of
https://github.com/kristoferssolo/kristofersxyz-rs.git
synced 2025-10-21 20:10:36 +00:00
feat: add configuration
This commit is contained in:
parent
c4d25790a4
commit
3e190e3cca
847
Cargo.lock
generated
847
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -20,8 +20,9 @@ console_error_panic_hook = "0.1.7"
|
||||
console_log = "1.0.0"
|
||||
http = "1.3.1"
|
||||
log = "0.4.27"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
simple_logger = "5.0.0"
|
||||
thiserror = "2.0.12"
|
||||
thiserror = "2"
|
||||
tokio = { version = "1.45", features = ["rt", "macros", "rt-multi-thread"] }
|
||||
tower = { version = "0.5.2", features = ["full"] }
|
||||
tower-http = { version = "0.6.4", features = ["full"] }
|
||||
|
||||
@ -11,12 +11,26 @@ leptos = { workspace = true, features = ["ssr"] }
|
||||
leptos_axum.workspace = true
|
||||
|
||||
axum.workspace = true
|
||||
simple_logger.workspace = true
|
||||
tokio = { workspace = true, features = ["tracing"] }
|
||||
tower.workspace = true
|
||||
tower-http.workspace = true
|
||||
config = { version = "0.15", features = ["toml"], default-features = false }
|
||||
log.workspace = true
|
||||
tracing-bunyan-formatter = { version = "0.3", default-features = false }
|
||||
secrecy = { version = "0.10", features = ["serde"] }
|
||||
serde-aux = "4.7"
|
||||
serde.workspace = true
|
||||
simple_logger.workspace = true
|
||||
sqlx = { version = "0.8", default-features = false, features = [
|
||||
"runtime-tokio",
|
||||
"tls-rustls",
|
||||
"macros",
|
||||
"postgres",
|
||||
"uuid",
|
||||
"chrono",
|
||||
"migrate",
|
||||
] }
|
||||
thiserror.workspace = true
|
||||
tokio = { workspace = true, features = ["tracing"] }
|
||||
tower-http.workspace = true
|
||||
tower.workspace = true
|
||||
tracing = { version = "0.1", features = ["log"] }
|
||||
tracing-bunyan-formatter = { version = "0.3", default-features = false }
|
||||
tracing-log = "0.2.0"
|
||||
tracing-subscriber = { version = "0.3", features = ["registry", "env-filter"] }
|
||||
|
||||
9
server/configuration/base.toml
Normal file
9
server/configuration/base.toml
Normal file
@ -0,0 +1,9 @@
|
||||
[application]
|
||||
port = 8000
|
||||
|
||||
[database]
|
||||
host = "127.0.0.1"
|
||||
port = 5432
|
||||
username = "postgres"
|
||||
password = "password"
|
||||
database_name = "kristofersxyz"
|
||||
5
server/configuration/local.toml
Normal file
5
server/configuration/local.toml
Normal file
@ -0,0 +1,5 @@
|
||||
[application]
|
||||
host = "127.0.0.1"
|
||||
|
||||
[database]
|
||||
require_ssl = false
|
||||
5
server/configuration/productionn.toml
Normal file
5
server/configuration/productionn.toml
Normal file
@ -0,0 +1,5 @@
|
||||
[application]
|
||||
host = "0.0.0.0"
|
||||
|
||||
[database]
|
||||
require_ssl = true
|
||||
140
server/src/configuration.rs
Normal file
140
server/src/configuration.rs
Normal file
@ -0,0 +1,140 @@
|
||||
use secrecy::{ExposeSecret, SecretString};
|
||||
use serde::Deserialize;
|
||||
use serde_aux::field_attributes::deserialize_number_from_string;
|
||||
use sqlx::{
|
||||
ConnectOptions,
|
||||
postgres::{PgConnectOptions, PgSslMode},
|
||||
};
|
||||
use std::{fmt::Display, str::FromStr};
|
||||
use thiserror::Error;
|
||||
|
||||
/// Top-level application settings.
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct Settings {
|
||||
pub database: DatabaseSettings,
|
||||
pub application: ApplicationSettings,
|
||||
}
|
||||
|
||||
/// Database-related settings.
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct DatabaseSettings {
|
||||
pub username: String,
|
||||
pub password: SecretString,
|
||||
#[serde(deserialize_with = "deserialize_number_from_string")]
|
||||
pub port: u16,
|
||||
pub host: String,
|
||||
pub database_name: String,
|
||||
pub require_ssl: bool,
|
||||
}
|
||||
|
||||
/// Application-related settings.
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct ApplicationSettings {
|
||||
#[serde(deserialize_with = "deserialize_number_from_string")]
|
||||
pub port: u16,
|
||||
pub host: String,
|
||||
}
|
||||
|
||||
/// Supported application environments.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Environment {
|
||||
Local,
|
||||
Production,
|
||||
}
|
||||
|
||||
/// Errors that can occur while loading configuration.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ConfigurationError {
|
||||
#[error("Failed to load configuration file: {0}")]
|
||||
ConfigFileError(#[from] config::ConfigError),
|
||||
#[error("Invalid environment: {0}")]
|
||||
InvalidEnvironment(String),
|
||||
}
|
||||
|
||||
/// Load the application configuration.
|
||||
///
|
||||
/// This function reads configuration files and environment variables to build
|
||||
/// the `Settings` struct.
|
||||
///
|
||||
/// - `base.toml` is always loaded.
|
||||
/// - `{environment}.toml` is loaded based on the `APP_ENVIRONMENT` variable.
|
||||
/// - Environment variables prefixed with `APP_` override file-based settings.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns a `ConfigurationError` if the configuration cannot be loaded.
|
||||
pub fn get_config() -> Result<Settings, ConfigurationError> {
|
||||
let base_path = std::env::current_dir().expect("Failed to determine current directory");
|
||||
let config_directory = base_path.join("config");
|
||||
let env = std::env::var("APP_ENVIRONMENT")
|
||||
.unwrap_or_else(|_| "local".into())
|
||||
.parse::<Environment>()?;
|
||||
|
||||
let env_filename = format!("{}.toml", &env);
|
||||
|
||||
let settings = config::Config::builder()
|
||||
.add_source(config::File::from(config_directory.join("base.toml")))
|
||||
.add_source(config::File::from(config_directory.join(env_filename)))
|
||||
.add_source(
|
||||
config::Environment::with_prefix("APP")
|
||||
.prefix_separator("_")
|
||||
.separator("__"),
|
||||
)
|
||||
.build()?;
|
||||
|
||||
settings
|
||||
.try_deserialize::<Settings>()
|
||||
.map_err(ConfigurationError::from)
|
||||
}
|
||||
|
||||
impl DatabaseSettings {
|
||||
/// Build connection options without specifying the database name.
|
||||
pub fn without_db(&self) -> PgConnectOptions {
|
||||
let ssl_mode = if self.require_ssl {
|
||||
PgSslMode::Require
|
||||
} else {
|
||||
PgSslMode::Prefer
|
||||
};
|
||||
|
||||
PgConnectOptions::new()
|
||||
.host(&self.host)
|
||||
.username(&self.username)
|
||||
.password(self.password.expose_secret())
|
||||
.port(self.port)
|
||||
.ssl_mode(ssl_mode)
|
||||
}
|
||||
|
||||
/// Build connection options with the database name.
|
||||
pub fn with_db(&self) -> PgConnectOptions {
|
||||
self.without_db()
|
||||
.database(&self.database_name)
|
||||
.log_statements(tracing_log::log::LevelFilter::Trace)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Environment {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let s = match self {
|
||||
Environment::Local => "local",
|
||||
Environment::Production => "production",
|
||||
};
|
||||
write!(f, "{s}")
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Environment {
|
||||
type Err = ConfigurationError;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"local" => Ok(Self::Local),
|
||||
"production" => Ok(Self::Production),
|
||||
other => Err(ConfigurationError::InvalidEnvironment(other.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for Environment {
|
||||
type Error = ConfigurationError;
|
||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||
value.parse()
|
||||
}
|
||||
}
|
||||
4
server/src/lib.rs
Normal file
4
server/src/lib.rs
Normal file
@ -0,0 +1,4 @@
|
||||
mod configuration;
|
||||
mod routes;
|
||||
mod startup;
|
||||
mod telemetry;
|
||||
@ -1,5 +1,3 @@
|
||||
mod telemetry;
|
||||
|
||||
use app::*;
|
||||
use axum::Router;
|
||||
use leptos::logging::log;
|
||||
|
||||
6
server/src/routes/mod.rs
Normal file
6
server/src/routes/mod.rs
Normal file
@ -0,0 +1,6 @@
|
||||
use crate::startup::AppState;
|
||||
use axum::Router;
|
||||
|
||||
pub fn route(state: AppState) -> Router {
|
||||
todo!()
|
||||
}
|
||||
77
server/src/startup.rs
Normal file
77
server/src/startup.rs
Normal file
@ -0,0 +1,77 @@
|
||||
use crate::{
|
||||
configuration::{DatabaseSettings, Settings},
|
||||
routes::route,
|
||||
};
|
||||
use sqlx::{PgPool, postgres::PgPoolOptions};
|
||||
use std::{
|
||||
io::{Error, ErrorKind},
|
||||
sync::Arc,
|
||||
};
|
||||
use tokio::{net::TcpListener, task::JoinHandle};
|
||||
use tracing::{error, info};
|
||||
|
||||
/// Shared application state.
|
||||
pub struct App {
|
||||
pub pool: PgPool,
|
||||
}
|
||||
|
||||
/// Type alias for the shared application state wrapped in `Arc`.
|
||||
pub type AppState = Arc<App>;
|
||||
|
||||
/// Represents the application, including its server and configuration.
|
||||
pub struct Application {
|
||||
port: u16,
|
||||
server: Option<JoinHandle<Result<(), Error>>>,
|
||||
}
|
||||
|
||||
impl Application {
|
||||
/// Build a new `Application` instance from the provided configuration.
|
||||
///
|
||||
/// This method initializes the database connection pool, binds the server
|
||||
/// to the specified address, and prepares the application for startup.
|
||||
pub async fn build(config: &Settings) -> Result<Self, Error> {
|
||||
// Initialize the database connection pool
|
||||
let pool = get_connection_pool(&config.database);
|
||||
info!("Database connection pool initialized.");
|
||||
|
||||
// Bind the server to the specified address
|
||||
let addr = format!("{}:{}", config.application.host, config.application.port);
|
||||
let listener = TcpListener::bind(addr).await?;
|
||||
let port = listener.local_addr().unwrap().port();
|
||||
info!("Server listening on port {}", port);
|
||||
|
||||
// Create the shared application state
|
||||
let app_state = App { pool }.into();
|
||||
|
||||
// Spawn the server
|
||||
let server = tokio::spawn(async move {
|
||||
axum::serve(listener, route(app_state)).await.map_err(|e| {
|
||||
error!("Server error: {}", e);
|
||||
e
|
||||
})
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
port,
|
||||
server: Some(server),
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the port the server is listening on.
|
||||
pub fn port(&self) -> u16 {
|
||||
self.port
|
||||
}
|
||||
|
||||
pub async fn start(self) -> Result<(), Error> {
|
||||
self.server
|
||||
.ok_or_else(|| Error::new(ErrorKind::Other, "Server was not initialized."))?
|
||||
.await?
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize the database connection pool.
|
||||
///
|
||||
/// This method creates a lazy connection pool using the provided database settings.
|
||||
fn get_connection_pool(config: &DatabaseSettings) -> PgPool {
|
||||
PgPoolOptions::new().connect_lazy_with(config.with_db())
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user