diff options
Diffstat (limited to 'lib/warden-core')
| -rw-r--r-- | lib/warden-core/Cargo.toml | 21 | ||||
| -rw-r--r-- | lib/warden-core/src/config/cli/database.rs | 136 | ||||
| -rw-r--r-- | lib/warden-core/src/config/cli/mod.rs | 85 | ||||
| -rw-r--r-- | lib/warden-core/src/config/log_level.rs | 68 | ||||
| -rw-r--r-- | lib/warden-core/src/config/mod.rs | 120 | ||||
| -rw-r--r-- | lib/warden-core/src/error.rs | 15 | ||||
| -rw-r--r-- | lib/warden-core/src/lib.rs | 3 |
7 files changed, 448 insertions, 0 deletions
diff --git a/lib/warden-core/Cargo.toml b/lib/warden-core/Cargo.toml new file mode 100644 index 0000000..1f92e87 --- /dev/null +++ b/lib/warden-core/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "warden-core" +version = "0.1.0" +edition = "2024" +license.workspace = true +readme.workspace = true +documentation.workspace = true +homepage.workspace = true +publish.workspace = true + +[dependencies] +clap = { workspace = true, features = ["derive", "env"] } +serde = { workspace = true, features = ["derive"] } +thiserror.workspace = true +tracing.workspace = true +tracing-subscriber = { version = "0.3.23", features = ["env-filter"] } +url.workspace = true + +[dependencies.sqlx] +workspace = true +features = ["json", "runtime-tokio-rustls", "time"] diff --git a/lib/warden-core/src/config/cli/database.rs b/lib/warden-core/src/config/cli/database.rs new file mode 100644 index 0000000..31ba930 --- /dev/null +++ b/lib/warden-core/src/config/cli/database.rs @@ -0,0 +1,136 @@ +use clap::Parser; +use serde::{Deserialize, Serialize}; +use url::Url; + +use crate::WardenError; + +#[derive(Parser, Clone, Deserialize, Serialize, Debug)] +#[serde(rename_all = "kebab-case")] +pub struct Database { + /// Full database URL (if provided, overrides individual components) + #[arg(long, env = "DATABASE_URL")] + pub database_url: Option<Url>, + + #[arg(long, env = "DB_USER")] + /// Database username + #[serde(rename = "username")] + pub database_username: Option<String>, + + /// Database password + #[arg(long, env = "DB_PASSWORD")] + #[serde(rename = "password")] + pub database_password: Option<String>, + + /// Database host + #[arg(long, env = "DB_HOST", default_value = "localhost")] + #[serde(rename = "host")] + pub database_host: Option<String>, + + /// Database port + #[arg(long, env = "DB_PORT")] + #[serde(rename = "port")] + pub database_port: Option<u16>, + + /// Database name + #[arg(long, env = "DB_NAME")] + #[serde(rename = "name")] + pub database_name: Option<String>, + + /// Database pool size + #[arg(long, env = "DATABASE_POOL_SIZE", default_value = "10")] + #[serde(rename = "pool-size")] + pub database_pool_size: Option<u32>, +} + +impl Default for Database { + fn default() -> Self { + Self { + database_url: Default::default(), + database_username: Some(String::from("postgres")), + database_password: Some(String::from("password")), + database_host: Some(String::from("localhost")), + database_port: Some(5432), + database_name: Some(String::from("warden")), + database_pool_size: Some(10), + } + } +} + +impl Database { + pub fn merge(cli: &Self, file: &Self) -> Result<Self, WardenError> { + let url = cli.database_url.clone().or(file.database_url.clone()); + + let pool_size = cli + .database_pool_size + .or(file.database_pool_size) + .unwrap_or(10); + + let final_url = match url { + Some(u) => u, + None => { + let host = cli + .database_host + .clone() + .or(file.database_host.clone()) + .unwrap_or_else(|| "localhost".to_string()); + + let mut u = Url::parse(&format!("postgresql://{}", host))?; + + let user = cli + .database_username + .as_ref() + .or(file.database_username.as_ref()); + let pass = cli + .database_password + .as_ref() + .or(file.database_password.as_ref()); + let port = cli.database_port.or(file.database_port); + let name = cli.database_name.as_ref().or(file.database_name.as_ref()); + + if let Some(user) = user { + u.set_username(user).ok(); + } + if let Some(pass) = pass { + u.set_password(Some(pass)).ok(); + } + if let Some(port) = port { + u.set_port(Some(port)).ok(); + } + if let Some(name) = name { + u.set_path(name); + } + + u + } + }; + + Ok(Self { + database_url: Some(final_url), + database_pool_size: Some(pool_size), + ..cli.clone() + }) + } + + pub fn get_url(&self) -> Result<Url, url::ParseError> { + if let Some(ref url) = self.database_url { + return Ok(url.clone()); + } + + let host = "localhost".to_owned(); + let host = self.database_host.as_ref().unwrap_or_else(|| &host); + let mut url = Url::parse(&format!("postgres://{host}"))?; + + if let Some(ref u) = self.database_username { + url.set_username(u).ok(); + } + if let Some(ref p) = self.database_password { + url.set_password(Some(p)).ok(); + } + url.set_port(self.database_port).ok(); + if let Some(ref name) = self.database_name { + url.set_path(name); + } + + Ok(url) + } +} diff --git a/lib/warden-core/src/config/cli/mod.rs b/lib/warden-core/src/config/cli/mod.rs new file mode 100644 index 0000000..36f6bf0 --- /dev/null +++ b/lib/warden-core/src/config/cli/mod.rs @@ -0,0 +1,85 @@ +pub mod database; +use std::path::PathBuf; + +use clap::{Parser, Subcommand, ValueEnum}; +use serde::{Deserialize, Serialize}; + +use crate::config::cli::database::Database; + +#[derive(Parser, Serialize, Deserialize, Default, Debug)] +#[command(version, about, long_about = None)] +/// Real-time transaction monitoring +pub struct Cli { + /// Sets a custom config file + #[arg(short, long, value_name = "FILE")] + #[serde(skip)] + pub config: Option<PathBuf>, + #[command(flatten)] + pub server: Server, + #[command(subcommand)] + #[serde(skip)] + pub command: Option<Commands>, + #[command(flatten)] + pub database: Database, +} + +#[derive(Subcommand, Debug)] +pub enum Commands { + /// Generates a default configuration file + Init { + /// Path to save the config + #[arg(short, long, default_value = "warden.toml")] + path: String, + }, +} + +#[derive(Parser, Deserialize, Serialize, Debug)] +#[serde(rename_all = "kebab-case")] +pub struct Server { + /// Sets the port that the server listens to + #[arg(short, long, value_name = "PORT", env = "PORT", default_value = "2210")] + #[arg(value_parser = clap::value_parser!(u16).range(1..=65535))] + pub port: Option<u16>, + /// Runtime environment + #[arg(short, long, value_name = "ENV", default_value = "prod", env = "ENV")] + pub environment: Option<CliEnvironment>, + /// Log Level + #[arg(long, value_name = "LOG_LEVEL", env = "LOG_LEVEL")] + pub log_level: Option<String>, + /// Log file directory (defaults to temp_dir) + #[arg(long, value_name = "DIR", env = "LOGS_DIR")] + pub log_dir: Option<PathBuf>, + /// Request timeout duration in seconds + #[arg( + long, + value_name = "TIMEOUT_DURATION", + env = "TIMEOUT_DURATION_SECS", + default_value = "5" + )] + pub timeout_secs: Option<u64>, +} + +impl Default for Server { + fn default() -> Self { + Self { + port: Some(2210), + environment: Default::default(), + log_level: Some(format!( + "{}=debug,tower_http=debug,axum::rejection=trace", + env!("CARGO_CRATE_NAME") + )), + log_dir: Some(std::env::temp_dir()), + timeout_secs: Some(5), + } + } +} + +#[derive(Deserialize, ValueEnum, Serialize, Clone, Copy, Default, Debug)] +#[serde(rename_all = "lowercase")] +pub enum CliEnvironment { + #[default] + Dev, + Development, + Prod, + Production, +} diff --git a/lib/warden-core/src/config/log_level.rs b/lib/warden-core/src/config/log_level.rs new file mode 100644 index 0000000..3edfbc3 --- /dev/null +++ b/lib/warden-core/src/config/log_level.rs @@ -0,0 +1,68 @@ +use clap::ValueEnum; +use serde::Deserialize; + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug, Default, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum LogLevel { + /// The "trace" level. + /// + /// Designates very low priority, often extremely verbose, information. + Trace = 0, + /// The "debug" level. + /// + /// Designates lower priority information. + #[default] + Debug = 1, + /// The "info" level. + /// + /// Designates useful information. + Info = 2, + /// The "warn" level. + /// + /// Designates hazardous situations. + Warn = 3, + /// The "error" level. + /// + /// Designates very serious errors. + Error = 4, +} + +impl From<LogLevel> for tracing::Level { + fn from(value: LogLevel) -> Self { + match value { + LogLevel::Trace => tracing::Level::TRACE, + LogLevel::Debug => tracing::Level::DEBUG, + LogLevel::Info => tracing::Level::INFO, + LogLevel::Warn => tracing::Level::WARN, + LogLevel::Error => tracing::Level::ERROR, + } + } +} + +#[cfg(test)] +mod tests { + use super::LogLevel; + + fn check(level: LogLevel, value: &str) { + let level = tracing::Level::from(level); + assert_eq!(level.to_string().to_lowercase(), value); + } + + #[test] + fn loglevel() { + let level = LogLevel::Trace; + check(level, "trace"); + + let level = LogLevel::Debug; + check(level, "debug"); + + let level = LogLevel::Info; + check(level, "info"); + + let level = LogLevel::Warn; + check(level, "warn"); + + let level = LogLevel::Error; + check(level, "error"); + } +} diff --git a/lib/warden-core/src/config/mod.rs b/lib/warden-core/src/config/mod.rs new file mode 100644 index 0000000..9d0c937 --- /dev/null +++ b/lib/warden-core/src/config/mod.rs @@ -0,0 +1,120 @@ +pub mod cli; +mod log_level; + +use std::path::PathBuf; + +use clap::ValueEnum; +pub use cli::Cli; +pub use cli::Commands; +use serde::Deserialize; +use tracing_subscriber::EnvFilter; + +use crate::WardenError; +use crate::config::cli::CliEnvironment; +use crate::config::cli::database::Database; + +#[derive(Deserialize, Default, Debug, ValueEnum, Clone, Copy)] +#[serde(rename_all = "lowercase")] +pub enum Environment { + Development, + #[default] + Production, +} + +impl From<CliEnvironment> for Environment { + fn from(value: CliEnvironment) -> Self { + match value { + CliEnvironment::Dev | CliEnvironment::Development => Self::Development, + CliEnvironment::Prod | CliEnvironment::Production => Self::Production, + } + } +} + +#[derive(Debug, Clone)] +pub struct Configuration { + pub server: Server, + pub database: Database, +} + +#[derive(Debug, Clone)] +pub struct Server { + pub port: u16, + pub environment: Environment, + pub log_level: EnvFilter, + pub log_dir: PathBuf, + pub timeout_secs: u64, +} + +impl Server { + fn merge(cli: &Cli, file: &Cli, missing: &mut Vec<&str>) -> Result<Self, WardenError> { + let port = cli.server.port.or(file.server.port); + + if port.is_none() { + missing.push("server.port"); + } + + let timeout = cli.server.timeout_secs.or(file.server.timeout_secs); + + if timeout.is_none() { + missing.push("server.timeout"); + } + + let log_dir = cli.server.log_dir.clone().or(file.server.log_dir.clone()); + + if log_dir.is_none() { + missing.push("server.log_dir"); + } + + let log_level = cli + .server + .log_level + .as_ref() + .or(file.server.log_level.as_ref()) + .map(ToOwned::to_owned); + + if log_level.is_none() { + missing.push("server.log_level"); + } + + let environment = cli.server.environment.or(file.server.environment); + + if environment.is_none() { + missing.push("server.environment"); + } + + if !missing.is_empty() { + let err = missing + .iter() + .map(|f| format!(" - {}", f)) + .collect::<Vec<_>>() + .join("\n"); + return Err(WardenError::Config(err)); + } + + let log_level = + tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| { + // axum logs rejections from built-in extractors with the `axum::rejection` + // target, at `TRACE` level. `axum::rejection=trace` enables showing those events + log_level.unwrap().into() + }); + + Ok(Self { + port: port.unwrap(), + environment: environment.unwrap().into(), + log_dir: log_dir.unwrap(), + timeout_secs: timeout.unwrap(), + log_level, + }) + } +} + +impl Configuration { + pub fn merge(cli: &Cli, file: &Cli) -> Result<Self, WardenError> { + let mut missing = Vec::new(); + + let server = Server::merge(cli, file, &mut missing)?; + let database = Database::merge(&cli.database, &file.database)?; + + Ok(Self { server, database }) + } +} diff --git a/lib/warden-core/src/error.rs b/lib/warden-core/src/error.rs new file mode 100644 index 0000000..f05971f --- /dev/null +++ b/lib/warden-core/src/error.rs @@ -0,0 +1,15 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum WardenError { + #[error(transparent)] + Datastore(#[from] sqlx::Error), + #[error(transparent)] + Url(#[from] url::ParseError), + #[error("Missing required configuration values:\n`{0}`")] + Config(String), + #[error("invalid header (expected {expected:?}, found {found:?})")] + InvalidHeader { expected: String, found: String }, + #[error("unknown data store error")] + Unknown, +} diff --git a/lib/warden-core/src/lib.rs b/lib/warden-core/src/lib.rs new file mode 100644 index 0000000..f200ba1 --- /dev/null +++ b/lib/warden-core/src/lib.rs @@ -0,0 +1,3 @@ +mod error; +pub use error::WardenError; +pub mod config; |
