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"
|
console_log = "1.0.0"
|
||||||
http = "1.3.1"
|
http = "1.3.1"
|
||||||
log = "0.4.27"
|
log = "0.4.27"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
simple_logger = "5.0.0"
|
simple_logger = "5.0.0"
|
||||||
thiserror = "2.0.12"
|
thiserror = "2"
|
||||||
tokio = { version = "1.45", features = ["rt", "macros", "rt-multi-thread"] }
|
tokio = { version = "1.45", features = ["rt", "macros", "rt-multi-thread"] }
|
||||||
tower = { version = "0.5.2", features = ["full"] }
|
tower = { version = "0.5.2", features = ["full"] }
|
||||||
tower-http = { version = "0.6.4", features = ["full"] }
|
tower-http = { version = "0.6.4", features = ["full"] }
|
||||||
|
|||||||
@ -11,12 +11,26 @@ leptos = { workspace = true, features = ["ssr"] }
|
|||||||
leptos_axum.workspace = true
|
leptos_axum.workspace = true
|
||||||
|
|
||||||
axum.workspace = true
|
axum.workspace = true
|
||||||
simple_logger.workspace = true
|
config = { version = "0.15", features = ["toml"], default-features = false }
|
||||||
tokio = { workspace = true, features = ["tracing"] }
|
|
||||||
tower.workspace = true
|
|
||||||
tower-http.workspace = true
|
|
||||||
log.workspace = true
|
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 = { version = "0.1", features = ["log"] }
|
||||||
|
tracing-bunyan-formatter = { version = "0.3", default-features = false }
|
||||||
tracing-log = "0.2.0"
|
tracing-log = "0.2.0"
|
||||||
tracing-subscriber = { version = "0.3", features = ["registry", "env-filter"] }
|
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 app::*;
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
use leptos::logging::log;
|
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