18 changed files with 307 additions and 5 deletions
@ -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" |
||||
@ -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<dyn AppCommand> { |
||||
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<String>, |
||||
} |
||||
|
||||
pub trait Configurable { |
||||
fn apply_defaults( |
||||
&self, |
||||
builder: ConfigBuilder<DefaultState>, |
||||
) -> Result<ConfigBuilder<DefaultState>>; |
||||
fn apply_overrides( |
||||
&self, |
||||
builder: ConfigBuilder<DefaultState>, |
||||
) -> Result<ConfigBuilder<DefaultState>>; |
||||
} |
||||
|
||||
#[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<T: Configurable + Executable> 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); |
||||
} |
||||
@ -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<String>, |
||||
|
||||
#[arg(short, long, help = defaults::HELP_LISTEN_PORT)] |
||||
pub port: Option<u16>, |
||||
} |
||||
|
||||
impl Configurable for ListenCmd { |
||||
fn apply_defaults( |
||||
&self, |
||||
builder: ConfigBuilder<DefaultState>, |
||||
) -> Result<ConfigBuilder<DefaultState>> { |
||||
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<DefaultState>, |
||||
) -> Result<ConfigBuilder<DefaultState>> { |
||||
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); |
||||
} |
||||
@ -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<listen::Config>, |
||||
pub log_level: String, |
||||
} |
||||
|
||||
impl AppConfig { |
||||
pub fn build(args: &GlobalArgs, handler: &dyn Configurable) -> Result<Self> { |
||||
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") |
||||
} |
||||
} |
||||
@ -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<Self> { |
||||
Ok(Self) |
||||
} |
||||
|
||||
pub async fn create_dict_importer(&self, dict_name: &str) -> anyhow::Result<DictImporter> { |
||||
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<Arc<dyn DictRepository>> { |
||||
let mut dict_repo = SqliteDictRepository::new("sqlite:app.db").await?; |
||||
dict_repo.use_dict(dict_name); |
||||
Ok(Arc::new(dict_repo)) |
||||
} |
||||
} |
||||
@ -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 |
||||
@ -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<dyn AppCommand>, |
||||
} |
||||
|
||||
impl Application { |
||||
pub async fn build() -> Result<Self> { |
||||
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 |
||||
} |
||||
} |
||||
@ -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(()) |
||||
} |
||||
Loading…
Reference in new issue