diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index f24fe9a..1027a78 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -17,8 +17,7 @@ "tamasfe.even-better-toml", "vadimcn.vscode-lldb", "frosticless.monokai-one-darker", - "ms-vscode.cpptools", - "serayuzgur.crates" + "ms-vscode.cpptools" ], "settings": { "workbench.colorTheme": "Monokai One Darker", diff --git a/app/Cargo.toml b/app/Cargo.toml index d8187de..f05ed3f 100644 --- a/app/Cargo.toml +++ b/app/Cargo.toml @@ -4,5 +4,20 @@ version = "0.1.0" edition = "2024" [dependencies] -clap = { version = "4.5.51", features = ["derive"] } +# Internal libmnemor = { path = "../lib" } + + +# Runtime & Async +tokio = { version = "1.48", features = ["full"] } +anyhow = "1.0" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +# Configuration & Inputs +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +toml = "0.9.8" +clap = { version = "4.5", features = ["derive", "env"] } +config = "0.15.19" +const_format = "0.2.35" \ No newline at end of file diff --git a/app/src/app.rs b/app/src/app.rs new file mode 100644 index 0000000..dfca7f7 --- /dev/null +++ b/app/src/app.rs @@ -0,0 +1,70 @@ +use crate::bootstrap::{AppConfig, CliArgs}; +use crate::container::Container; +use anyhow::Result; +use clap::Parser; +use tokio::signal; +use tracing::{info, warn}; + +/// The Main Application entry point. +pub struct Application { + config: AppConfig, + container: Container, +} + +impl Application { + /// Bootstrap: Loads config, initializes logging, wires dependencies. + pub async fn build() -> Result { + let args = CliArgs::parse(); + + // 1. Load Config + let config = AppConfig::build(&args)?; + + // 2. Init Logging (Tracing) + tracing_subscriber::fmt() + .with_env_filter(&config.log_level) + .init(); + + info!("Bootstrapping application..."); + + // 3. Wire Dependencies + let container = Container::new(&config).await?; + + Ok(Self { config, container }) + } + + /// Execution: Starts the main loop and waits for shutdown signal. + pub async fn run(self) -> Result<()> { + info!("Application started on port {}", self.config.server.port); + + // Example: Spawn a web server or background worker here. + // We pass a clone of the container or specific services. + let server_task = tokio::spawn(server_loop(self.config.clone(), self.container.clone())); + + // Wait for Shutdown Signal + match signal::ctrl_c().await { + Ok(()) => { + info!("Received shutdown signal (SIGINT/SIGTERM)"); + } + Err(err) => { + warn!("Failed to listen for shutdown signal: {}", err); + } + } + + // Graceful Shutdown Logic + info!("Shutting down..."); + server_task.abort(); // Or send a specialized shutdown channel message + + Ok(()) + } +} + +/// Simulated long-running process (e.g., HTTP Server) +async fn server_loop(config: AppConfig, container: Container) { + loop { + tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; + info!("Health check... DB URL: {}", config.database.url); + + // Example usage of the container + // let _ = container.user_service.do_something(); + } +} diff --git a/app/src/bootstrap.rs b/app/src/bootstrap.rs new file mode 100644 index 0000000..fb3e05f --- /dev/null +++ b/app/src/bootstrap.rs @@ -0,0 +1,6 @@ +mod app_config; +mod cli; +mod defaults; + +pub use self::app_config::AppConfig; +pub use self::cli::Args as CliArgs; diff --git a/app/src/bootstrap/app_config.rs b/app/src/bootstrap/app_config.rs new file mode 100644 index 0000000..a9f6562 --- /dev/null +++ b/app/src/bootstrap/app_config.rs @@ -0,0 +1,57 @@ +use anyhow::{Context, Result}; +use config::{Config, Environment, File}; +use serde::Deserialize; + +use super::cli::Args; +use super::defaults::set_defaults; + +#[derive(Debug, Deserialize, Clone)] +pub struct AppConfig { + pub server: ServerConfig, + pub database: DatabaseConfig, + pub log_level: String, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct ServerConfig { + pub host: String, + pub port: u16, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct DatabaseConfig { + pub url: String, + pub max_connections: u32, +} + +impl AppConfig { + pub fn build(args: &Args) -> Result { + let mut builder = Config::builder(); + + // Defaults + builder = set_defaults(builder)?; + + // File Layer + let config_path = &args.config; + let is_default_path = config_path.to_str() == Some("config.toml"); + + builder = builder.add_source(File::from(config_path.as_path()).required(!is_default_path)); + + // Environment Layer (APP__SERVER__PORT) + builder = builder.add_source(Environment::with_prefix("APP").separator("__")); + + // CLI Overrides Layer + if let Some(port) = args.port { + builder = builder.set_override("server.port", port)?; + } + if let Some(ref level) = args.log_level { + builder = builder.set_override("log_level", level.clone())?; + } + + builder + .build() + .context("Failed to build configuration layers")? + .try_deserialize() + .context("Failed to deserialize Config") + } +} diff --git a/app/src/bootstrap/cli.rs b/app/src/bootstrap/cli.rs new file mode 100644 index 0000000..5e40b06 --- /dev/null +++ b/app/src/bootstrap/cli.rs @@ -0,0 +1,17 @@ +use super::defaults; +use clap::Parser; +use std::path::PathBuf; + +#[derive(Parser, Debug)] +#[command(author, version, about)] +pub struct Args { + /// Path to config file + #[arg(short, long, default_value = "config.toml")] + pub config: PathBuf, + + #[arg(short, long, help = defaults::HELP_PORT)] + pub port: Option, + + #[arg(long, help = defaults::HELP_LOG)] + pub log_level: Option, +} diff --git a/app/src/bootstrap/defaults.rs b/app/src/bootstrap/defaults.rs new file mode 100644 index 0000000..b3514d3 --- /dev/null +++ b/app/src/bootstrap/defaults.rs @@ -0,0 +1,27 @@ +use anyhow::Result; +pub use config::ConfigBuilder; + +use const_format::formatcp; + +pub const HOST: &str = "127.0.0.1"; +pub const PORT: u16 = 8080; +pub const LOG_LEVEL: &str = "info"; +pub const DB_URL: &str = "postgres://localhost:5432/myapp"; +pub const DB_MAX_CONNS: u32 = 5; + +pub const HELP_PORT: &str = formatcp!("Override Port [default: {}]", PORT); +pub const HELP_LOG: &str = formatcp!("Override Log Level [default: {}]", LOG_LEVEL); + +pub fn set_defaults( + builder: ConfigBuilder, +) -> Result> { + builder + .set_default("log_level", LOG_LEVEL)? + // Server + .set_default("server.host", HOST)? + .set_default("server.port", PORT)? + // Database + .set_default("database.url", DB_URL)? + .set_default("database.max_connections", DB_MAX_CONNS) + .map_err(|e| e.into()) +} diff --git a/app/src/container.rs b/app/src/container.rs new file mode 100644 index 0000000..d28f4b4 --- /dev/null +++ b/app/src/container.rs @@ -0,0 +1,24 @@ +use crate::bootstrap::AppConfig; +// use std::sync::Arc; + +#[derive(Clone)] +pub struct Container { + // pub user_service: Arc, + // Add other services here (e.g., EmailService, MetricsService) +} + +impl Container { + pub async fn new(config: &AppConfig) -> anyhow::Result { + // 1. Infrastructure / Adapters + // Using the config to initialize connections + // let user_repo = PostgresUserRepository::new(&AppConfig.database.url).await?; + // let user_repo = Arc::new(user_repo); + + // // 2. Application / Domain Services + // // Injecting the repository into the service + // let user_service = UserService::new(user_repo); + // let user_service = Arc::new(user_service); + + Ok(Self {}) + } +} diff --git a/app/src/main.rs b/app/src/main.rs index c68d052..3fc04e4 100644 --- a/app/src/main.rs +++ b/app/src/main.rs @@ -1,61 +1,74 @@ -use clap::{Parser, ValueEnum}; -use libmnemor::{System, create_encoder}; - -#[derive(ValueEnum, Debug, Clone, Copy)] -#[clap(rename_all = "kebab-case")] -enum SystemCli { - MajorEn, - MajorPl, -} +// use clap::{Parser, ValueEnum}; +// use libmnemor::{System, create_encoder}; -impl From for System { - fn from(cli_system: SystemCli) -> Self { - match cli_system { - SystemCli::MajorEn => System::MajorEn, - SystemCli::MajorPl => System::MajorPl, - } - } -} +// #[derive(ValueEnum, Debug, Clone, Copy)] +// #[clap(rename_all = "kebab-case")] +// enum SystemCli { +// MajorEn, +// MajorPl, +// } -#[derive(Parser)] -#[command(author, version, about)] -struct CliArgs { - /// System name - #[arg(short, long, default_value = "major-pl")] - system: SystemCli, +// impl From for System { +// fn from(cli_system: SystemCli) -> Self { +// match cli_system { +// SystemCli::MajorEn => System::MajorEn, +// SystemCli::MajorPl => System::MajorPl, +// } +// } +// } - /// Encode given word - #[arg(short, long)] - encode: Option, +// #[derive(Parser)] +// #[command(author, version, about)] +// struct CliArgs { +// /// System name +// #[arg(short, long, default_value = "major-pl")] +// system: SystemCli, - /// List supported systems - #[arg(long)] - list_systems: bool, -} +// /// Encode given word +// #[arg(short, long)] +// encode: Option, + +// /// List supported systems +// #[arg(long)] +// list_systems: bool, +// } + +// fn main() -> Result<(), Box> { +// let args = CliArgs::parse(); + +// if args.list_systems { +// println!("Supported systems:"); +// for system in SystemCli::value_variants() { +// if let Some(possible_value) = system.to_possible_value() { +// println!("- {}", possible_value.get_name()); +// } +// } +// return Ok(()); +// } + +// if let Some(word) = args.encode { +// println!( +// "Encoding {} with system {:?}...", +// word, +// args.system.to_possible_value().unwrap().get_name() +// ); +// let encoder = create_encoder(&args.system.into()); +// let encoded_word = encoder.encode(&word); +// println!("Encoded: [{}]", encoded_word); +// } + +// Ok(()) +// } -fn main() -> Result<(), Box> { - let args = CliArgs::parse(); - - if args.list_systems { - println!("Supported systems:"); - for system in SystemCli::value_variants() { - if let Some(possible_value) = system.to_possible_value() { - println!("- {}", possible_value.get_name()); - } - } - return Ok(()); - } - - if let Some(word) = args.encode { - println!( - "Encoding {} with system {:?}...", - word, - args.system.to_possible_value().unwrap().get_name() - ); - let encoder = create_encoder(&args.system.into()); - let encoded_word = encoder.encode(&word); - println!("Encoded: [{}]", encoded_word); - } +mod app; +mod bootstrap; +mod container; +use anyhow::Result; +use app::Application; +#[tokio::main] +async fn main() -> Result<()> { + let app = Application::build().await?; + app.run().await?; Ok(()) }