aboutsummaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/warden-core/Cargo.toml21
-rw-r--r--lib/warden-core/src/config/cli/database.rs136
-rw-r--r--lib/warden-core/src/config/cli/mod.rs85
-rw-r--r--lib/warden-core/src/config/log_level.rs68
-rw-r--r--lib/warden-core/src/config/mod.rs120
-rw-r--r--lib/warden-core/src/error.rs15
-rw-r--r--lib/warden-core/src/lib.rs3
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;