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] |
[package] |
||||||
name = "phomnemic" |
name = "phomnemic-cli" |
||||||
version = "0.1.0" |
version = "0.1.0" |
||||||
edition = "2024" |
edition = "2024" |
||||||
|
|
||||||
[dependencies] |
[dependencies] |
||||||
# Internal |
# Internal |
||||||
applib = { path = "../lib" } |
applib = { path = "../../lib" } |
||||||
|
|
||||||
|
|
||||||
# Runtime & Async |
# 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