diff --git a/Cargo.toml b/Cargo.toml index 8ffd4b0..e3217b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,6 @@ resolver = "3" [workspace.package] edition = "2024" authors = ["chodak166 "] -license = "MIT" [profile.release] lto = true # Link Time Optimization: Analyzes entire program for optimizations diff --git a/app/Cargo.toml b/app/Cargo.toml index f05ed3f..5a8838f 100644 --- a/app/Cargo.toml +++ b/app/Cargo.toml @@ -1,11 +1,11 @@ [package] -name = "mnemo-r" +name = "phomnemic" version = "0.1.0" edition = "2024" [dependencies] # Internal -libmnemor = { path = "../lib" } +applib = { path = "../lib" } # Runtime & Async @@ -20,4 +20,4 @@ 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 +const_format = "0.2.35" diff --git a/app/src/app.rs b/app/src/app.rs index ae5b817..c82717d 100644 --- a/app/src/app.rs +++ b/app/src/app.rs @@ -1,13 +1,11 @@ -use crate::bootstrap::cli::Command; -use crate::bootstrap::{AppConfig, CliArgs}; -use crate::commands; +use crate::config::AppConfig; use crate::container::Container; use anyhow::Result; +use applib::cli::{CliArgs, Command, commands}; use clap::Parser; use tokio::signal; -use tracing::{info, warn}; +use tracing::{debug, info, warn}; -/// The Main Application entry point. pub struct Application { config: AppConfig, container: Container, @@ -15,28 +13,17 @@ pub struct Application { } impl Application { - /// Bootstrap: Loads config, initializes logging, wires dependencies. pub async fn build() -> Result { let args = CliArgs::parse(); - // 1. Determine Port Override (only for Server command) - let port_override = if let Command::Server(ref server_args) = args.command { - server_args.port - } else { - None - }; + let config = AppConfig::build(&args.global, &args.command)?; - // 2. Load Config - let config = AppConfig::build(&args.global, port_override)?; - - // 3. Init Logging (Tracing) tracing_subscriber::fmt() .with_env_filter(&config.log_level) .init(); - info!("Bootstrapping application..."); + debug!("Bootstrapping application..."); - // 4. Wire Dependencies let container = Container::new(&config).await?; Ok(Self { @@ -46,37 +33,26 @@ impl Application { }) } - /// Execution: Starts the main loop and waits for shutdown signal. pub async fn run(self) -> Result<()> { match self.command { Command::Server(_) => { - info!("Application started on port {}", self.config.server.port); - - // Spawn the server loop - let server_task = tokio::spawn(commands::server::run( - 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 + commands::server::run(self.config.server, Self::wait_for_shutdown_signal()).await; } - Command::ExportDict(export_args) => { - commands::export::run(export_args, self.config, self.container).await; + Command::Encode(_) => { + commands::encode::run(self.config.encoder).await; } } - Ok(()) } + + async fn 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); + } + } + } } diff --git a/app/src/bootstrap.rs b/app/src/bootstrap.rs deleted file mode 100644 index 2510748..0000000 --- a/app/src/bootstrap.rs +++ /dev/null @@ -1,6 +0,0 @@ -mod app_config; -pub mod cli; // Make cli public so we can access subcommands -mod defaults; - -pub use self::app_config::AppConfig; -pub use self::cli::Args as CliArgs; diff --git a/app/src/commands/export.rs b/app/src/commands/export.rs deleted file mode 100644 index 71bbecd..0000000 --- a/app/src/commands/export.rs +++ /dev/null @@ -1,12 +0,0 @@ -use crate::bootstrap::AppConfig; -use crate::bootstrap::cli::ExportDictArgs; -use crate::container::Container; -use tracing::info; - -pub async fn run(args: ExportDictArgs, _config: AppConfig, _container: Container) { - info!("Exporting dictionary '{}' to {:?}", args.name, args.output); - - // Logic for export would go here - // e.g. let dict = container.dict_service.get(args.name); - // encoder.write(dict, args.output); -} diff --git a/app/src/commands/server.rs b/app/src/commands/server.rs deleted file mode 100644 index 14d8337..0000000 --- a/app/src/commands/server.rs +++ /dev/null @@ -1,13 +0,0 @@ -use crate::bootstrap::AppConfig; -use crate::container::Container; -use tracing::info; - -pub async fn run(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/app_config.rs b/app/src/config.rs similarity index 55% rename from app/src/bootstrap/app_config.rs rename to app/src/config.rs index 8d680b2..e96f8d2 100644 --- a/app/src/bootstrap/app_config.rs +++ b/app/src/config.rs @@ -2,30 +2,18 @@ use anyhow::{Context, Result}; use config::{Config, Environment, File}; use serde::Deserialize; -use super::cli::GlobalArgs; -use super::defaults::set_defaults; +use applib::cli::{Command, GlobalArgs, defaults::set_defaults}; +use applib::config::{EncoderConfig, ServerConfig}; #[derive(Debug, Deserialize, Clone)] pub struct AppConfig { pub server: ServerConfig, - pub database: DatabaseConfig, + pub encoder: EncoderConfig, 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: &GlobalArgs, port_override: Option) -> Result { + pub fn build(args: &GlobalArgs, command: &Command) -> Result { let mut builder = Config::builder(); // Defaults @@ -37,17 +25,28 @@ impl AppConfig { 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("__")); + // Environment Layer (APP_SERVER_PORT) + builder = builder.add_source(Environment::with_prefix("APP").separator("_")); // CLI Overrides Layer - if let Some(port) = port_override { - builder = builder.set_override("server.port", port)?; - } if let Some(ref level) = args.log_level { builder = builder.set_override("log_level", level.clone())?; } + match command { + Command::Server(cmd_args) => { + if let Some(port) = cmd_args.port { + builder = builder.set_override("server.port", port)?; + } + } + Command::Encode(cmd_args) => { + if let Some(name) = &cmd_args.system { + builder = builder.set_override("encoder.system", name.as_str())?; + } + builder = builder.set_override("encoder.input", cmd_args.input.clone())?; + } + } + builder .build() .context("Failed to build configuration layers")? diff --git a/app/src/container.rs b/app/src/container.rs index d28f4b4..06759ca 100644 --- a/app/src/container.rs +++ b/app/src/container.rs @@ -1,24 +1,10 @@ -use crate::bootstrap::AppConfig; -// use std::sync::Arc; +use crate::config::AppConfig; #[derive(Clone)] -pub struct Container { - // pub user_service: Arc, - // Add other services here (e.g., EmailService, MetricsService) -} +pub struct Container {} 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); - + pub async fn new(_: &AppConfig) -> anyhow::Result { Ok(Self {}) } } diff --git a/app/src/main.rs b/app/src/main.rs index d2dc8a5..c201d83 100644 --- a/app/src/main.rs +++ b/app/src/main.rs @@ -1,6 +1,5 @@ mod app; -mod bootstrap; -mod commands; +mod config; mod container; use anyhow::Result; @@ -12,3 +11,13 @@ async fn main() -> Result<()> { app.run().await?; Ok(()) } + +// use applib::Greeter; +// use applib::infrastructure::std_greeting_printer::StdGreetingPrinter; +// use std::sync::Arc; + +// fn main() { +// let printer = Arc::new(StdGreetingPrinter::new()); +// let greeter = Greeter::new(printer); +// greeter.say_hello("John"); +// } diff --git a/config.toml b/config.toml new file mode 100644 index 0000000..ff38c9f --- /dev/null +++ b/config.toml @@ -0,0 +1,6 @@ +# Logging configuration +log_level = "info" + +# Server configuration +[server] +port = 8080 diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 3dc33c3..0262aa7 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -1,7 +1,18 @@ [package] -name = "libmnemor" +name = "applib" version = "0.1.0" edition = "2024" [dependencies] once_cell = "1.21.3" +clap = { version = "4.5", features = ["derive", "env"] } +const_format = "0.2.35" +config = "0.15.19" +tracing = "0.1" +tokio = { version = "1.48", features = ["full"] } +anyhow = "1.0" +serde = { version = "1.0", features = ["derive"] } +chrono = { version = "0.4", features = ["serde"] } +thiserror = "1.0" +async-trait = "0.1" +parking_lot = "0.12" diff --git a/lib/src/application.rs b/lib/src/application.rs new file mode 100644 index 0000000..37d536f --- /dev/null +++ b/lib/src/application.rs @@ -0,0 +1,4 @@ +pub mod config; +pub mod errors; +pub mod services; +pub mod traits; diff --git a/lib/src/application/config.rs b/lib/src/application/config.rs new file mode 100644 index 0000000..aab67d0 --- /dev/null +++ b/lib/src/application/config.rs @@ -0,0 +1,13 @@ +use crate::core::system::System; +use serde::Deserialize; + +#[derive(Debug, Deserialize, Clone)] +pub struct ServerConfig { + pub port: u16, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct EncoderConfig { + pub system: System, + pub input: String, +} diff --git a/lib/src/application/errors.rs b/lib/src/application/errors.rs new file mode 100644 index 0000000..9c34269 --- /dev/null +++ b/lib/src/application/errors.rs @@ -0,0 +1,13 @@ +#[derive(Debug)] +pub enum RepositoryError { + NotFound, + ConnectionFailed, + InvalidData(String), + Unexpected(String), +} + +impl std::fmt::Display for RepositoryError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} diff --git a/lib/src/application/services.rs b/lib/src/application/services.rs new file mode 100644 index 0000000..fe05113 --- /dev/null +++ b/lib/src/application/services.rs @@ -0,0 +1,53 @@ +use crate::application::traits::{DictRepository, DictSource}; +use crate::core::entities::{Dict, DictEntry}; + +pub struct DictImporter<'a, R> { + repo: &'a R, + batch_size: usize, +} + +impl<'a, R: DictRepository> DictImporter<'a, R> { + pub fn new(repo: &'a R) -> Self { + Self { + repo, + batch_size: 1000, // reasonable default + } + } + + pub fn import(&self, name: &str, mut source: impl DictSource) -> Result<(), anyhow::Error> { + // 1. Ensure Dict exists (Logic: Create if new, or maybe clear existing?) + self.repo.create(name)?; + + let mut batch = Vec::with_capacity(self.batch_size); + + // 2. Stream data + while let Some(result) = source.next_entry() { + match result { + Ok(entry) => { + // Optional: Domain Validation logic could go here + // if entry.text.is_empty() { continue; } + + batch.push(entry); + + // 3. Batch Write + if batch.len() >= self.batch_size { + self.repo.save_entries(name, &batch)?; + batch.clear(); + } + } + Err(e) => { + // Logic: Do we abort on malformed JSON or log and continue? + // Here we abort for safety. + return Err(e); + } + } + } + + // 4. Flush remaining + if !batch.is_empty() { + self.repo.save_entries(name, &batch)?; + } + + Ok(()) + } +} diff --git a/lib/src/application/traits.rs b/lib/src/application/traits.rs new file mode 100644 index 0000000..27deaba --- /dev/null +++ b/lib/src/application/traits.rs @@ -0,0 +1,21 @@ +use crate::{ + application::errors::RepositoryError, + core::entities::{Dict, DictEntry}, +}; + +pub trait DictRepository { + fn create(&self, name: &str) -> Result<(), RepositoryError>; + // Batch saving is usually much faster than 1-by-1 for SQL + fn save_entries(&self, dict_name: &str, entries: &[DictEntry]) -> Result<(), RepositoryError>; + + fn fetch_many( + &self, + name: &str, + limit: Option, + offset: Option, + ) -> Result; +} + +pub trait DictSource { + fn next_entry(&mut self) -> Option>; +} diff --git a/lib/src/core/mod.rs b/lib/src/core.rs similarity index 53% rename from lib/src/core/mod.rs rename to lib/src/core.rs index fc7a881..4877415 100644 --- a/lib/src/core/mod.rs +++ b/lib/src/core.rs @@ -1,11 +1,11 @@ pub mod entities; pub mod errors; -pub mod major; +pub mod sys_major; pub mod system; pub mod traits; // pub use self::major::*; -pub use self::entities::*; -pub use self::errors::*; -pub use self::system::*; +// pub use self::entities::*; +// pub use self::errors::*; +// pub use self::system::*; pub use self::traits::*; diff --git a/lib/src/core/entities.rs b/lib/src/core/entities.rs index deaa7b1..dec4e40 100644 --- a/lib/src/core/entities.rs +++ b/lib/src/core/entities.rs @@ -1,45 +1,38 @@ use std::collections::HashMap; -#[derive(Debug, Default, Clone)] -pub struct DictEntry { - pub phoneme_in: String, - pub phoneme_out: String, - - pub not_before: Vec, - pub not_after: Vec, +pub type DictEntryId = u32; - pub only_before: Vec, - pub only_after: Vec, +#[derive(Debug, Clone, PartialEq)] +pub struct DictEntry { + pub id: DictEntryId, + pub text: String, + pub metadata: HashMap, } impl DictEntry { - pub fn into_lowercase(self) -> Self { + pub fn new(id: DictEntryId, text: String) -> Self { DictEntry { - phoneme_in: self.phoneme_in.to_lowercase(), - phoneme_out: self.phoneme_out.to_lowercase(), - not_before: Self::lower_vec(self.not_before), - not_after: Self::lower_vec(self.not_after), - only_before: Self::lower_vec(self.only_before), - only_after: Self::lower_vec(self.only_after), + id, + text, + metadata: HashMap::new(), } } - - fn lower_vec(vec: Vec) -> Vec { - vec.into_iter().map(|s| s.to_lowercase()).collect() - } } -pub type DictEntries = Vec; pub struct Dict { - name: String, - entries: DictEntries, + pub name: String, + pub entries: HashMap, } -pub type WordEntryId = u32; +impl Dict { + pub fn new(name: String) -> Self { + Dict { + name, + entries: HashMap::new(), + } + } -#[derive(Debug, Default)] -pub struct WordEntry { - id: Option, - word: String, - metadata: HashMap, + pub fn add_entry(&mut self, entry: DictEntry) { + self.entries.insert(entry.id, entry); + } } diff --git a/lib/src/core/errors.rs b/lib/src/core/errors.rs index 703cc7f..758a094 100644 --- a/lib/src/core/errors.rs +++ b/lib/src/core/errors.rs @@ -1,15 +1,15 @@ -use std::fmt; +// use std::fmt; -#[derive(Debug)] -pub enum RepositoryError { - NotFound, - ConnectionFailed, - InvalidData(String), - Unexpected(String), -} +// #[derive(Debug)] +// pub enum RepositoryError { +// NotFound, +// ConnectionFailed, +// InvalidData(String), +// Unexpected(String), +// } -impl fmt::Display for RepositoryError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:?}", self) - } -} +// impl fmt::Display for RepositoryError { +// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +// write!(f, "{:?}", self) +// } +// } diff --git a/lib/src/core/major.rs b/lib/src/core/sys_major.rs similarity index 65% rename from lib/src/core/major.rs rename to lib/src/core/sys_major.rs index 206925e..72574b2 100644 --- a/lib/src/core/major.rs +++ b/lib/src/core/sys_major.rs @@ -1,6 +1,6 @@ -pub mod dict_en; -pub mod dict_pl; mod encoder; +pub mod rules_en; +pub mod rules_pl; #[cfg(test)] mod encoder_tests; diff --git a/lib/src/core/major/encoder.rs b/lib/src/core/sys_major/encoder.rs similarity index 56% rename from lib/src/core/major/encoder.rs rename to lib/src/core/sys_major/encoder.rs index c226c86..43c9783 100644 --- a/lib/src/core/major/encoder.rs +++ b/lib/src/core/sys_major/encoder.rs @@ -1,34 +1,69 @@ -use crate::core::entities::{DictEntries, DictEntry}; use crate::core::traits::SystemEncoder; +#[derive(Debug, Default, Clone)] +pub struct Rule { + pub phoneme_in: String, + pub phoneme_out: String, + + pub not_before: Vec, + pub not_after: Vec, + + pub only_before: Vec, + pub only_after: Vec, +} + +impl Rule { + pub fn into_lowercase(self) -> Self { + Rule { + phoneme_in: self.phoneme_in.to_lowercase(), + phoneme_out: self.phoneme_out.to_lowercase(), + not_before: Self::lower_vec(self.not_before), + not_after: Self::lower_vec(self.not_after), + only_before: Self::lower_vec(self.only_before), + only_after: Self::lower_vec(self.only_after), + } + } + + fn lower_vec(vec: Vec) -> Vec { + vec.into_iter().map(|s| s.to_lowercase()).collect() + } +} + +pub type Rules = Vec; +// pub struct rules { +// name: String, +// entries: Rules, +// } + /// (index, encoded value) -type DictMatches = Vec<(usize, String)>; +type RuleMatches = Vec<(usize, String)>; pub struct Encoder { - dict: DictEntries, + rules: Rules, } impl Encoder { - pub fn new(dict: DictEntries) -> Self { + pub fn new(rules: Rules) -> Self { Encoder { - dict: Encoder::to_lower_dict(dict), + rules: Encoder::to_lower_rules(rules), } } - fn to_lower_dict(dict: DictEntries) -> DictEntries { - dict.into_iter() + fn to_lower_rules(rules: Rules) -> Rules { + rules + .into_iter() .map(|entry| entry.into_lowercase()) .collect() } - fn match_entry(&self, entry: &DictEntry, word: &str) -> DictMatches { + fn match_entry(&self, entry: &Rule, word: &str) -> RuleMatches { word.match_indices(&entry.phoneme_in) .filter(|(index, _)| self.is_context_matched(&entry, &word, *index)) .map(|(index, _)| (index, entry.phoneme_out.clone())) .collect() } - fn is_context_matched(&self, entry: &DictEntry, word: &str, index: usize) -> bool { + fn is_context_matched(&self, entry: &Rule, word: &str, index: usize) -> bool { let before_context = &word[..index]; let after_context = &word[index + entry.phoneme_in.len()..]; dbg!(&before_context); @@ -74,8 +109,8 @@ impl Encoder { impl SystemEncoder for Encoder { fn encode(&self, word: &str) -> String { - let mut matches: DictMatches = self - .dict + let mut matches: RuleMatches = self + .rules .iter() .flat_map(|entry| self.match_entry(&entry, &word.to_lowercase())) .collect(); diff --git a/lib/src/core/major/encoder_tests.rs b/lib/src/core/sys_major/encoder_tests.rs similarity index 92% rename from lib/src/core/major/encoder_tests.rs rename to lib/src/core/sys_major/encoder_tests.rs index 635220c..78becd2 100644 --- a/lib/src/core/major/encoder_tests.rs +++ b/lib/src/core/sys_major/encoder_tests.rs @@ -1,13 +1,12 @@ -use crate::core::entities::{DictEntries, DictEntry}; -use crate::core::major::*; +use super::encoder::{Encoder, Rule, Rules}; use crate::core::traits::SystemEncoder; #[cfg(test)] mod tests { use super::*; - fn create_single_dict() -> DictEntries { - vec![DictEntry { + fn create_single_dict() -> Rules { + vec![Rule { phoneme_in: "B".to_string(), phoneme_out: "2".to_string(), not_after: vec!["Y".to_string()], @@ -17,17 +16,17 @@ mod tests { }] } - fn create_single_dict_min() -> DictEntries { - vec![DictEntry { + fn create_single_dict_min() -> Rules { + vec![Rule { phoneme_in: "B".to_string(), phoneme_out: "2".to_string(), ..Default::default() }] } - fn create_double_dict() -> DictEntries { + fn create_double_dict() -> Rules { vec![ - DictEntry { + Rule { phoneme_in: "CD".to_string(), phoneme_out: "2".to_string(), not_after: vec!["00".to_string(), "YZ".to_string()], @@ -35,7 +34,7 @@ mod tests { only_after: vec!["22".to_string(), "AB".to_string()], only_before: vec!["33".to_string(), "EF".to_string()], }, - DictEntry { + Rule { phoneme_in: "MN".to_string(), phoneme_out: "3".to_string(), ..Default::default() diff --git a/lib/src/core/major/dict_en.rs b/lib/src/core/sys_major/rules_en.rs similarity index 75% rename from lib/src/core/major/dict_en.rs rename to lib/src/core/sys_major/rules_en.rs index 37292df..a904e9b 100644 --- a/lib/src/core/major/dict_en.rs +++ b/lib/src/core/sys_major/rules_en.rs @@ -1,8 +1,8 @@ -use crate::core::entities::{DictEntries, DictEntry}; +use super::encoder::{Rule, Rules}; -pub fn get_dict() -> DictEntries { +pub fn get_rules() -> Rules { vec![ - DictEntry { + Rule { phoneme_in: "EN".to_string(), phoneme_out: "2".to_string(), not_after: vec!["Y".to_string()], diff --git a/lib/src/core/major/dict_pl.rs b/lib/src/core/sys_major/rules_pl.rs similarity index 75% rename from lib/src/core/major/dict_pl.rs rename to lib/src/core/sys_major/rules_pl.rs index 36562e2..ac6054a 100644 --- a/lib/src/core/major/dict_pl.rs +++ b/lib/src/core/sys_major/rules_pl.rs @@ -1,8 +1,8 @@ -use crate::core::entities::{DictEntries, DictEntry}; +use super::encoder::{Rule, Rules}; -pub fn get_dict() -> DictEntries { +pub fn get_rules() -> Rules { vec![ - DictEntry { + Rule { phoneme_in: "PL".to_string(), phoneme_out: "2".to_string(), not_after: vec!["Y".to_string()], diff --git a/lib/src/core/system.rs b/lib/src/core/system.rs index 2f0dd13..761dad7 100644 --- a/lib/src/core/system.rs +++ b/lib/src/core/system.rs @@ -1,15 +1,30 @@ +use serde::Deserialize; + use crate::core::SystemEncoder; -use crate::core::major; +use crate::core::sys_major as major; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] pub enum System { + #[serde(rename = "major_en")] MajorEn, + #[serde(rename = "major_pl")] MajorPl, } +// from: +impl From<&str> for System { + fn from(s: &str) -> Self { + match s { + "major_en" => System::MajorEn, + "major_pl" => System::MajorPl, + _ => panic!("Unknown system: {}", s), + } + } +} + pub fn create_encoder(system: &System) -> Box { match system { - System::MajorPl => Box::new(major::Encoder::new(major::dict_pl::get_dict())), - System::MajorEn => Box::new(major::Encoder::new(major::dict_en::get_dict())), + System::MajorPl => Box::new(major::Encoder::new(major::rules_pl::get_rules())), + System::MajorEn => Box::new(major::Encoder::new(major::rules_en::get_rules())), } } diff --git a/lib/src/core/traits.rs b/lib/src/core/traits.rs index 6195afd..8753b86 100644 --- a/lib/src/core/traits.rs +++ b/lib/src/core/traits.rs @@ -1,18 +1,15 @@ -use crate::core::entities::{Dict, WordEntry, WordEntryId}; -use crate::core::errors::RepositoryError; - pub trait SystemEncoder { fn encode(&self, word: &str) -> String; } -pub trait WordRepository { - fn save(word: &WordEntry) -> Result; - fn save_many(words: &Vec) -> Result<(), RepositoryError>; - fn fetch(id: WordEntryId) -> Result; - fn fetch_many(ids: &Vec) -> Result, RepositoryError>; -} +// pub trait WordRepository { +// fn save(word: &WordEntry) -> Result; +// fn save_many(words: &Vec) -> Result<(), RepositoryError>; +// fn fetch(id: WordEntryId) -> Result; +// fn fetch_many(ids: &Vec) -> Result, RepositoryError>; +// } -pub trait DictRepository { - fn save(dict: &Dict) -> Result<(), RepositoryError>; - fn fetch(name: &str) -> Result; -} +// pub trait DictRepository { +// fn save(dict: &Dict) -> Result<(), RepositoryError>; +// fn fetch(name: &str) -> Result; +// } diff --git a/lib/src/infrastructure.rs b/lib/src/infrastructure.rs new file mode 100644 index 0000000..629e98f --- /dev/null +++ b/lib/src/infrastructure.rs @@ -0,0 +1 @@ +pub mod errors; diff --git a/lib/src/infrastructure/errors.rs b/lib/src/infrastructure/errors.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/lib/src/infrastructure/errors.rs @@ -0,0 +1 @@ + diff --git a/lib/src/infrastructure/json_file_dict_source.rs b/lib/src/infrastructure/json_file_dict_source.rs new file mode 100644 index 0000000..542fb0e --- /dev/null +++ b/lib/src/infrastructure/json_file_dict_source.rs @@ -0,0 +1,45 @@ +use crate::application::ports::DictSource; +use crate::core::entities::{DictEntry, DictEntryId}; +use serde::Deserialize; +use std::fs::File; +use std::io::BufReader; +use std::path::Path; + +// The "Wire Format". +// It exists ONLY here to map external JSON names to internal Entity names. +#[derive(Deserialize)] +struct JsonEntry { + id: u32, + word: String, // "word" in JSON, "text" in Entity + // If JSON has extra fields we don't care about, Serde ignores them. +} + +pub struct JsonFileDictSource { + // Helper iterator from Serde + iter: + serde_json::StreamDeserializer<'static, serde_json::de::IoRead>, JsonEntry>, +} + +impl JsonFileDictSource { + pub fn new>(path: P) -> anyhow::Result { + let file = File::open(path)?; + let reader = BufReader::new(file); + let iter = serde_json::Deserializer::from_reader(reader).into_iter::(); + Ok(Self { iter }) + } +} + +impl DictSource for JsonFileDictSource { + fn next_entry(&mut self) -> Option> { + self.iter.next().map(|res| { + match res { + Ok(json) => { + // MAPPING HAPPENS HERE. + // This is type-safe. If DictEntry::new signature changes, this breaks. + Ok(DictEntry::new(json.id as DictEntryId, json.word)) + } + Err(e) => Err(anyhow::Error::new(e)), + } + }) + } +} diff --git a/lib/src/core/major/indexer.rs b/lib/src/infrastructure/sqlite_dict_repository.rs similarity index 100% rename from lib/src/core/major/indexer.rs rename to lib/src/infrastructure/sqlite_dict_repository.rs diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 8bdfb00..cc9b6d8 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,36 +1,9 @@ -// pub mod core; - +mod application; mod core; +pub mod infrastructure; +mod presentation; -pub use self::core::System; -pub use self::core::SystemEncoder; -pub use self::core::create_encoder; - -// pub struct TextProcessor { -// prefix: String, -// } - -// impl TextProcessor { -// /// Create a new processor with custom prefix -// pub fn new(prefix: &str) -> Self { -// Self { -// prefix: prefix.to_string(), -// } -// } - -// /// Process input text by adding prefix and converting to uppercase -// pub fn process(&self, input: &str) -> String { -// format!("{} {}", self.prefix, input.to_uppercase()) -// } -// } - -// #[cfg(test)] -// mod tests { -// use super::*; - -// #[test] -// fn test_processing() { -// let processor = TextProcessor::new(">> "); -// assert_eq!(processor.process("hello"), ">> HELLO"); -// } -// } +pub use self::application::config; +pub use self::core::system; +pub use self::core::traits::SystemEncoder; +pub use self::presentation::cli; diff --git a/lib/src/presentation.rs b/lib/src/presentation.rs new file mode 100644 index 0000000..4f77372 --- /dev/null +++ b/lib/src/presentation.rs @@ -0,0 +1 @@ +pub mod cli; diff --git a/lib/src/presentation/cli.rs b/lib/src/presentation/cli.rs new file mode 100644 index 0000000..846d92d --- /dev/null +++ b/lib/src/presentation/cli.rs @@ -0,0 +1,7 @@ +pub mod cli_args; +pub mod commands; +pub mod defaults; + +pub use self::cli_args::CliArgs; +pub use self::cli_args::Command; +pub use self::cli_args::GlobalArgs; diff --git a/app/src/bootstrap/cli.rs b/lib/src/presentation/cli/cli_args.rs similarity index 59% rename from app/src/bootstrap/cli.rs rename to lib/src/presentation/cli/cli_args.rs index 0f5df14..7e414b2 100644 --- a/app/src/bootstrap/cli.rs +++ b/lib/src/presentation/cli/cli_args.rs @@ -1,10 +1,10 @@ -use super::defaults; +use crate::cli::defaults; use clap::{Args as ClapArgs, Parser, Subcommand}; use std::path::PathBuf; #[derive(Parser, Debug)] #[command(author, version, about)] -pub struct Args { +pub struct CliArgs { #[command(flatten)] pub global: GlobalArgs, @@ -27,8 +27,8 @@ pub enum Command { /// Start the application server Server(ServerArgs), - /// Export dictionary to file - ExportDict(ExportDictArgs), + /// Encode a word using given system + Encode(EncodeArgs), } #[derive(ClapArgs, Debug, Clone)] @@ -38,12 +38,19 @@ pub struct ServerArgs { } #[derive(ClapArgs, Debug, Clone)] -pub struct ExportDictArgs { - /// Name of the dictionary to export - #[arg(long)] - pub name: String, - - /// Output file path - #[arg(long)] - pub output: PathBuf, +pub struct EncodeArgs { + #[arg(long, help = defaults::HELP_ENC_SYSTEM)] + pub system: Option, + + #[arg(long, help = defaults::HELP_ENC_INPUT)] + pub input: String, +} + +#[derive(ClapArgs, Debug, Clone)] +pub struct ImportDictArgs { + #[arg(long, help = defaults::HELP_IMPORT_DICT_NAME)] + pub name: Option, + + #[arg(long, help = defaults::HELP_IMPORT_DICT_INPUT)] + pub path: String, } diff --git a/app/src/commands.rs b/lib/src/presentation/cli/commands.rs similarity index 50% rename from app/src/commands.rs rename to lib/src/presentation/cli/commands.rs index 8df7eb2..0aca2a9 100644 --- a/app/src/commands.rs +++ b/lib/src/presentation/cli/commands.rs @@ -1,2 +1,2 @@ -pub mod export; +pub mod encode; pub mod server; diff --git a/lib/src/presentation/cli/commands/encode.rs b/lib/src/presentation/cli/commands/encode.rs new file mode 100644 index 0000000..624d4a2 --- /dev/null +++ b/lib/src/presentation/cli/commands/encode.rs @@ -0,0 +1,10 @@ +use crate::application::config::EncoderConfig; +use crate::core::system; +use tracing::debug; + +pub async fn run(config: EncoderConfig) { + debug!("Running greeter with config {:?}", config); + let encoder = system::create_encoder(&config.system); + let result = encoder.encode(&config.input); + println!("{}", result); +} diff --git a/lib/src/presentation/cli/commands/server.rs b/lib/src/presentation/cli/commands/server.rs new file mode 100644 index 0000000..7878a16 --- /dev/null +++ b/lib/src/presentation/cli/commands/server.rs @@ -0,0 +1,20 @@ +use crate::application::config::ServerConfig; +use tracing::info; + +pub async fn run(config: ServerConfig, blocker: impl std::future::Future) { + info!("Running server with config: {:#?}", config); + + tokio::select! { + _ = server_loop() => {}, + _ = blocker => { + info!("Shutting down server..."); + } + } +} + +async fn server_loop() { + loop { + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + info!("Health check... "); + } +} diff --git a/app/src/bootstrap/defaults.rs b/lib/src/presentation/cli/defaults.rs similarity index 69% rename from app/src/bootstrap/defaults.rs rename to lib/src/presentation/cli/defaults.rs index b3514d3..6def5a8 100644 --- a/app/src/bootstrap/defaults.rs +++ b/lib/src/presentation/cli/defaults.rs @@ -6,11 +6,12 @@ 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 SYSTEM_NAME: &str = "major_pl"; pub const HELP_PORT: &str = formatcp!("Override Port [default: {}]", PORT); pub const HELP_LOG: &str = formatcp!("Override Log Level [default: {}]", LOG_LEVEL); +pub const HELP_ENC_SYSTEM: &str = formatcp!("System to use [default: {}]", SYSTEM_NAME); +pub const HELP_ENC_INPUT: &str = formatcp!("Text to encode"); pub fn set_defaults( builder: ConfigBuilder, @@ -20,8 +21,8 @@ pub fn set_defaults( // 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) + // Encode + .set_default("encode.system", SYSTEM_NAME) + // Wrapping in Result .map_err(|e| e.into()) }