From dbf77e98761b3d7e3bc59d55e30e7c7230d07c6a Mon Sep 17 00:00:00 2001 From: chodak166 Date: Wed, 14 Jan 2026 18:57:28 +0100 Subject: [PATCH] Added app_cli/app_api crates --- Cargo.toml | 3 +- apps/app_api/Cargo.toml | 24 ++++++ {app => apps/app_api}/src/app.rs | 0 apps/app_api/src/commands.rs | 74 +++++++++++++++++++ apps/app_api/src/commands/listen.rs | 73 ++++++++++++++++++ apps/app_api/src/config.rs | 43 +++++++++++ apps/app_api/src/container.rs | 31 ++++++++ {app => apps/app_api}/src/main.rs | 0 {app => apps/app_cli}/Cargo.toml | 4 +- apps/app_cli/src/app.rs | 42 +++++++++++ {app => apps/app_cli}/src/commands.rs | 0 {app => apps/app_cli}/src/commands/decode.rs | 0 {app => apps/app_cli}/src/commands/encode.rs | 0 .../app_cli}/src/commands/import_dict.rs | 0 {app => apps/app_cli}/src/config.rs | 0 {app => apps/app_cli}/src/container.rs | 0 apps/app_cli/src/main.rs | 14 ++++ lib/src/sys_major/rules_pl.rs | 4 +- 18 files changed, 307 insertions(+), 5 deletions(-) create mode 100644 apps/app_api/Cargo.toml rename {app => apps/app_api}/src/app.rs (100%) create mode 100644 apps/app_api/src/commands.rs create mode 100644 apps/app_api/src/commands/listen.rs create mode 100644 apps/app_api/src/config.rs create mode 100644 apps/app_api/src/container.rs rename {app => apps/app_api}/src/main.rs (100%) rename {app => apps/app_cli}/Cargo.toml (89%) create mode 100644 apps/app_cli/src/app.rs rename {app => apps/app_cli}/src/commands.rs (100%) rename {app => apps/app_cli}/src/commands/decode.rs (100%) rename {app => apps/app_cli}/src/commands/encode.rs (100%) rename {app => apps/app_cli}/src/commands/import_dict.rs (100%) rename {app => apps/app_cli}/src/config.rs (100%) rename {app => apps/app_cli}/src/container.rs (100%) create mode 100644 apps/app_cli/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index e3217b7..0e113e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ - "app", + "apps/app_api", + "apps/app_cli", "lib", ] resolver = "3" diff --git a/apps/app_api/Cargo.toml b/apps/app_api/Cargo.toml new file mode 100644 index 0000000..464aa4f --- /dev/null +++ b/apps/app_api/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "phomnemic-server" +version = "0.1.0" +edition = "2024" + +[dependencies] +# Internal +applib = { path = "../../lib" } + + +# Runtime & Async +tokio = { version = "1.48", features = ["full"] } +anyhow = "1.0" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +async-trait = "0.1" + +# 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" diff --git a/app/src/app.rs b/apps/app_api/src/app.rs similarity index 100% rename from app/src/app.rs rename to apps/app_api/src/app.rs diff --git a/apps/app_api/src/commands.rs b/apps/app_api/src/commands.rs new file mode 100644 index 0000000..b34b510 --- /dev/null +++ b/apps/app_api/src/commands.rs @@ -0,0 +1,74 @@ +pub mod listen; + +use crate::config::AppConfig; +use crate::container::Container; +use anyhow::Result; +use async_trait::async_trait; +use clap::Subcommand; +use clap::{Args as ClapArgs, Parser}; +use config::ConfigBuilder; +use config::builder::DefaultState; +use std::path::PathBuf; + +#[derive(Subcommand, Debug, Clone)] +pub enum Command { + /// Decode a word using given system + Listen(listen::ListenCmd), +} + +impl Command { + pub fn into_app_command(self) -> Box { + match self { + Command::Listen(cmd) => Box::new(cmd), + } + } +} + +#[derive(Parser, Debug)] +#[command(author, version, about)] +pub struct CliArgs { + #[command(flatten)] + pub global: GlobalArgs, + + #[command(subcommand)] + pub command: Command, +} + +#[derive(ClapArgs, Debug)] +pub struct GlobalArgs { + /// Path to config file + #[arg(short, long, default_value = "config.toml")] + pub config: PathBuf, + + #[arg(long, help = defaults::HELP_LOG)] + pub log_level: Option, +} + +pub trait Configurable { + fn apply_defaults( + &self, + builder: ConfigBuilder, + ) -> Result>; + fn apply_overrides( + &self, + builder: ConfigBuilder, + ) -> Result>; +} + +#[async_trait] +pub trait Executable { + async fn execute(&self, config: &AppConfig, container: &Container) -> Result<()>; +} + +// AppCommand must be dyn-compatible. Configurable is already dyn-compatible. +// Executable is dyn-compatible because of #[async_trait]. +pub trait AppCommand: Configurable + Executable {} + +impl AppCommand for T {} + +mod defaults { + use const_format::formatcp; + + pub const LOG_LEVEL: &str = "info"; + pub const HELP_LOG: &str = formatcp!("Override Log Level [default: {}]", LOG_LEVEL); +} diff --git a/apps/app_api/src/commands/listen.rs b/apps/app_api/src/commands/listen.rs new file mode 100644 index 0000000..bb9a83f --- /dev/null +++ b/apps/app_api/src/commands/listen.rs @@ -0,0 +1,73 @@ +use crate::commands::{ClapArgs, Configurable, Executable}; +use crate::config::AppConfig; +use crate::container::Container; + +use anyhow::Result; +use async_trait::async_trait; +use config::ConfigBuilder; +use config::builder::DefaultState; +use serde::Deserialize; + +#[derive(Debug, Deserialize, Clone)] +pub struct Config { + pub host: String, + pub port: u16, +} + +#[derive(ClapArgs, Debug, Clone)] +pub struct ListenCmd { + #[arg(short, long, help = defaults::HELP_LISTEN_HOST)] + pub host: Option, + + #[arg(short, long, help = defaults::HELP_LISTEN_PORT)] + pub port: Option, +} + +impl Configurable for ListenCmd { + fn apply_defaults( + &self, + builder: ConfigBuilder, + ) -> Result> { + builder + .set_default("listen.host", defaults::LISTEN_HOST)? + .set_default("listen.port", defaults::LISTEN_PORT) + .map_err(Into::into) + } + + fn apply_overrides( + &self, + builder: ConfigBuilder, + ) -> Result> { + let mut builder = builder; + + if let Some(host) = &self.host { + builder = builder.set_override("listen.host", host.clone())?; + } + if let Some(port) = &self.port { + builder = builder.set_override("listen.port", port.clone())?; + } + Ok(builder) + } +} + +#[async_trait] +impl Executable for ListenCmd { + async fn execute(&self, config: &AppConfig, container: &Container) -> Result<()> { + let config = config + .listen + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Decoder config missing"))?; + + // TODO: start axum server + + Ok(()) + } +} + +mod defaults { + use const_format::formatcp; + pub const LISTEN_HOST: &str = "0.0.0.0"; + pub const LISTEN_PORT: u16 = 3000; + pub const HELP_LISTEN_HOST: &str = formatcp!("Host address [default: {}]", LISTEN_HOST); + pub const HELP_LISTEN_PORT: &str = formatcp!("Port to listen on [default: {}]", LISTEN_PORT); +} diff --git a/apps/app_api/src/config.rs b/apps/app_api/src/config.rs new file mode 100644 index 0000000..aabfd08 --- /dev/null +++ b/apps/app_api/src/config.rs @@ -0,0 +1,43 @@ +use crate::commands::*; +use anyhow::{Context, Result}; +use config::{Config, Environment, File}; +use serde::Deserialize; + +#[derive(Debug, Deserialize, Clone)] +pub struct AppConfig { + #[serde(default)] + pub listen: Option, + pub log_level: String, +} + +impl AppConfig { + pub fn build(args: &GlobalArgs, handler: &dyn Configurable) -> Result { + let mut builder = Config::builder(); + + // Command-specific defaults via Trait + builder = handler.apply_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 (e.g. APP_LISTEN_PORT) + builder = builder.add_source(Environment::with_prefix("APP").separator("_")); + + // Global log level override + if let Some(ref level) = args.log_level { + builder = builder.set_override("log_level", level.clone())?; + } + + // Command-specific overrides via Trait + builder = handler.apply_overrides(builder)?; + + builder + .build() + .context("Failed to build configuration layers")? + .try_deserialize() + .context("Failed to deserialize Config") + } +} diff --git a/apps/app_api/src/container.rs b/apps/app_api/src/container.rs new file mode 100644 index 0000000..d021855 --- /dev/null +++ b/apps/app_api/src/container.rs @@ -0,0 +1,31 @@ +use std::sync::Arc; + +use applib::DictImporter; +use applib::DictRepository; +use applib::SqliteDictRepository; +// use applib::SystemDecoder; +// use applib::SystemEncoder; +// use applib::sys_major as major; + +#[derive(Clone)] +pub struct Container; + +impl Container { + pub async fn new() -> anyhow::Result { + Ok(Self) + } + + pub async fn create_dict_importer(&self, dict_name: &str) -> anyhow::Result { + let repo = self.create_dict_repo(dict_name).await?; + Ok(DictImporter::new(repo)) + } + + pub async fn create_dict_repo( + &self, + dict_name: &str, + ) -> anyhow::Result> { + let mut dict_repo = SqliteDictRepository::new("sqlite:app.db").await?; + dict_repo.use_dict(dict_name); + Ok(Arc::new(dict_repo)) + } +} diff --git a/app/src/main.rs b/apps/app_api/src/main.rs similarity index 100% rename from app/src/main.rs rename to apps/app_api/src/main.rs diff --git a/app/Cargo.toml b/apps/app_cli/Cargo.toml similarity index 89% rename from app/Cargo.toml rename to apps/app_cli/Cargo.toml index 7f87eb2..1ea3260 100644 --- a/app/Cargo.toml +++ b/apps/app_cli/Cargo.toml @@ -1,11 +1,11 @@ [package] -name = "phomnemic" +name = "phomnemic-cli" version = "0.1.0" edition = "2024" [dependencies] # Internal -applib = { path = "../lib" } +applib = { path = "../../lib" } # Runtime & Async diff --git a/apps/app_cli/src/app.rs b/apps/app_cli/src/app.rs new file mode 100644 index 0000000..a5accbf --- /dev/null +++ b/apps/app_cli/src/app.rs @@ -0,0 +1,42 @@ +use crate::commands::{AppCommand, CliArgs}; +use crate::config::AppConfig; +use crate::container::Container; +use anyhow::Result; +use clap::Parser; +use tracing::debug; + +pub struct Application { + config: AppConfig, + container: Container, + command: Box, +} + +impl Application { + pub async fn build() -> Result { + let args = CliArgs::parse(); + + let app_cmd = args.command.into_app_command(); + + let config = AppConfig::build(&args.global, app_cmd.as_ref())?; + + tracing_subscriber::fmt() + .compact() + .with_env_filter(&config.log_level) + .with_target(false) + .init(); + + debug!("Bootstrapping application..."); + + let container = Container::new().await?; + + Ok(Self { + config, + container, + command: app_cmd, + }) + } + + pub async fn run(self) -> Result<()> { + self.command.execute(&self.config, &self.container).await + } +} diff --git a/app/src/commands.rs b/apps/app_cli/src/commands.rs similarity index 100% rename from app/src/commands.rs rename to apps/app_cli/src/commands.rs diff --git a/app/src/commands/decode.rs b/apps/app_cli/src/commands/decode.rs similarity index 100% rename from app/src/commands/decode.rs rename to apps/app_cli/src/commands/decode.rs diff --git a/app/src/commands/encode.rs b/apps/app_cli/src/commands/encode.rs similarity index 100% rename from app/src/commands/encode.rs rename to apps/app_cli/src/commands/encode.rs diff --git a/app/src/commands/import_dict.rs b/apps/app_cli/src/commands/import_dict.rs similarity index 100% rename from app/src/commands/import_dict.rs rename to apps/app_cli/src/commands/import_dict.rs diff --git a/app/src/config.rs b/apps/app_cli/src/config.rs similarity index 100% rename from app/src/config.rs rename to apps/app_cli/src/config.rs diff --git a/app/src/container.rs b/apps/app_cli/src/container.rs similarity index 100% rename from app/src/container.rs rename to apps/app_cli/src/container.rs diff --git a/apps/app_cli/src/main.rs b/apps/app_cli/src/main.rs new file mode 100644 index 0000000..377c91b --- /dev/null +++ b/apps/app_cli/src/main.rs @@ -0,0 +1,14 @@ +mod app; +mod commands; +mod config; +mod container; + +use anyhow::Result; +use app::Application; + +#[tokio::main] +async fn main() -> Result<()> { + let app = Application::build().await?; + app.run().await?; + Ok(()) +} diff --git a/lib/src/sys_major/rules_pl.rs b/lib/src/sys_major/rules_pl.rs index 0cdf587..6b45ab3 100644 --- a/lib/src/sys_major/rules_pl.rs +++ b/lib/src/sys_major/rules_pl.rs @@ -133,8 +133,8 @@ pub fn get_rules() -> Rules { #[cfg(test)] mod tests { use super::*; - use crate::core::sys_major::Decoder; - use crate::traits::SystemDecoder; + use crate::SystemDecoder; + use crate::sys_major::Decoder; #[test] fn test_major_dict_pl_decode_0_1() {