Compare commits
5 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
a5489495f4 | 4 months ago |
|
|
911a9a0256 | 4 months ago |
|
|
ea48899627 | 4 months ago |
|
|
846008de8e | 4 months ago |
|
|
1d67dab036 | 4 months ago |
24 changed files with 532 additions and 187 deletions
@ -0,0 +1,14 @@
|
||||
use crate::state::AppState; |
||||
use axum::Router; |
||||
use std::sync::Arc; |
||||
|
||||
pub mod dictionary; |
||||
pub mod health; |
||||
pub mod major_pl; |
||||
|
||||
pub fn routes() -> Router<Arc<AppState>> { |
||||
Router::new() |
||||
.nest("/api", health::routes()) |
||||
.nest("/api", dictionary::routes()) |
||||
.nest("/api", major_pl::routes()) |
||||
} |
||||
@ -0,0 +1,78 @@
|
||||
use axum::{Json, Router, extract::State, http::StatusCode, response::IntoResponse, routing::get}; |
||||
use serde::Serialize; |
||||
use std::sync::Arc; |
||||
|
||||
use crate::state::AppState; |
||||
|
||||
// --- DTOs ---
|
||||
|
||||
#[derive(Debug, Serialize)] |
||||
pub struct DictListResponse { |
||||
pub dictionaries: Vec<DictListEntryResponse>, |
||||
} |
||||
|
||||
#[derive(Debug, Serialize)] |
||||
pub struct DictListEntryResponse { |
||||
pub name: String, |
||||
pub entry_count: u64, |
||||
} |
||||
|
||||
#[derive(Debug, Serialize)] |
||||
pub struct ErrorResponse { |
||||
pub error: String, |
||||
} |
||||
|
||||
impl IntoResponse for ErrorResponse { |
||||
fn into_response(self) -> axum::response::Response { |
||||
(StatusCode::INTERNAL_SERVER_ERROR, Json(self)).into_response() |
||||
} |
||||
} |
||||
|
||||
impl From<anyhow::Error> for ErrorResponse { |
||||
fn from(err: anyhow::Error) -> Self { |
||||
Self { |
||||
error: err.to_string(), |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl From<applib::RepositoryError> for ErrorResponse { |
||||
fn from(err: applib::RepositoryError) -> Self { |
||||
Self { |
||||
error: err.to_string(), |
||||
} |
||||
} |
||||
} |
||||
|
||||
// --- Handlers ---
|
||||
|
||||
pub async fn list_dicts_handler( |
||||
State(state): State<Arc<AppState>>, |
||||
) -> Result<Json<DictListResponse>, ErrorResponse> { |
||||
let default_repo = state.container.create_dict_repo("default").await?; |
||||
|
||||
let dict_names = default_repo.fetch_dicts().await?; |
||||
|
||||
let mut entries = Vec::with_capacity(dict_names.len()); |
||||
|
||||
for dict_name in dict_names { |
||||
let dict_repo = state.container.create_dict_repo(&dict_name).await?; |
||||
|
||||
let entry_count = dict_repo.count_entries().await?; |
||||
|
||||
entries.push(DictListEntryResponse { |
||||
name: dict_name, |
||||
entry_count, |
||||
}); |
||||
} |
||||
|
||||
Ok(Json(DictListResponse { |
||||
dictionaries: entries, |
||||
})) |
||||
} |
||||
|
||||
// --- Router ---
|
||||
|
||||
pub fn routes() -> Router<Arc<AppState>> { |
||||
Router::new().route("/dicts", get(list_dicts_handler)) |
||||
} |
||||
@ -0,0 +1,74 @@
|
||||
use axum::{ |
||||
Json, Router, |
||||
extract::State, |
||||
http::StatusCode, |
||||
response::IntoResponse, |
||||
routing::{get, post}, |
||||
}; |
||||
use chrono::Utc; |
||||
use serde::Serialize; |
||||
use serde_json::Value; |
||||
use std::sync::Arc; |
||||
|
||||
use crate::state::AppState; |
||||
|
||||
// --- DTOs ---
|
||||
|
||||
#[derive(Debug, Serialize)] |
||||
pub struct EchoResponse { |
||||
pub data: Value, |
||||
pub timestamp: String, |
||||
} |
||||
|
||||
#[derive(Debug, Serialize)] |
||||
pub struct VersionResponse { |
||||
pub name: String, |
||||
pub version: String, |
||||
} |
||||
|
||||
#[derive(Debug, Serialize)] |
||||
pub struct ErrorResponse { |
||||
pub error: String, |
||||
} |
||||
|
||||
impl IntoResponse for ErrorResponse { |
||||
fn into_response(self) -> axum::response::Response { |
||||
(StatusCode::INTERNAL_SERVER_ERROR, Json(self)).into_response() |
||||
} |
||||
} |
||||
|
||||
impl<E: std::error::Error> From<E> for ErrorResponse { |
||||
fn from(err: E) -> Self { |
||||
Self { |
||||
error: err.to_string(), |
||||
} |
||||
} |
||||
} |
||||
|
||||
// --- Handlers ---
|
||||
|
||||
pub async fn echo_handler( |
||||
State(_state): State<Arc<AppState>>, |
||||
Json(payload): Json<Value>, |
||||
) -> Result<Json<EchoResponse>, ErrorResponse> { |
||||
let response = EchoResponse { |
||||
data: payload, |
||||
timestamp: Utc::now().to_rfc3339(), |
||||
}; |
||||
Ok(Json(response)) |
||||
} |
||||
|
||||
pub async fn version_handler(State(state): State<Arc<AppState>>) -> Json<VersionResponse> { |
||||
Json(VersionResponse { |
||||
name: state.name.clone(), |
||||
version: state.version.clone(), |
||||
}) |
||||
} |
||||
|
||||
// --- Router ---
|
||||
|
||||
pub fn routes() -> Router<Arc<AppState>> { |
||||
Router::new() |
||||
.route("/echo", post(echo_handler)) |
||||
.route("/version", get(version_handler)) |
||||
} |
||||
@ -0,0 +1,119 @@
|
||||
use axum::{ |
||||
Json, Router, |
||||
extract::{Path, Query, State}, |
||||
http::StatusCode, |
||||
response::IntoResponse, |
||||
routing::get, |
||||
}; |
||||
use serde::{Deserialize, Serialize}; |
||||
use std::sync::Arc; |
||||
|
||||
use crate::state::AppState; |
||||
|
||||
#[derive(Debug, Deserialize)] |
||||
pub struct EncodeQuery { |
||||
pub dict: Option<String>, |
||||
} |
||||
|
||||
#[derive(Debug, Deserialize)] |
||||
pub struct DecodeQuery { |
||||
pub dict: Option<String>, |
||||
} |
||||
|
||||
#[derive(Debug, Serialize)] |
||||
pub struct EncodeResponse { |
||||
pub input: String, |
||||
pub dict: String, |
||||
pub result: Vec<Vec<EncodePart>>, |
||||
} |
||||
|
||||
#[derive(Debug, Serialize)] |
||||
pub struct EncodePart { |
||||
pub value: u64, |
||||
pub words: Vec<String>, |
||||
} |
||||
|
||||
#[derive(Debug, Serialize)] |
||||
pub struct DecodeResponse { |
||||
pub input: String, |
||||
pub result: String, |
||||
} |
||||
|
||||
#[derive(Debug, Serialize)] |
||||
pub struct ErrorResponse { |
||||
pub error: String, |
||||
} |
||||
|
||||
impl IntoResponse for ErrorResponse { |
||||
fn into_response(self) -> axum::response::Response { |
||||
(StatusCode::INTERNAL_SERVER_ERROR, Json(self)).into_response() |
||||
} |
||||
} |
||||
|
||||
impl From<anyhow::Error> for ErrorResponse { |
||||
fn from(err: anyhow::Error) -> Self { |
||||
Self { |
||||
error: err.to_string(), |
||||
} |
||||
} |
||||
} |
||||
|
||||
pub async fn encode_handler( |
||||
State(state): State<Arc<AppState>>, |
||||
Path(input): Path<String>, |
||||
Query(params): Query<EncodeQuery>, |
||||
) -> Result<Json<EncodeResponse>, ErrorResponse> { |
||||
let dict_name = params.dict.unwrap_or_else(|| "demo_pl".to_string()); |
||||
let encoder = state |
||||
.container |
||||
.create_encoder(&dict_name) |
||||
.await |
||||
.map_err(|e| anyhow::anyhow!("Failed to create encoder: {}", e))?; |
||||
let result = encoder |
||||
.encode(&input) |
||||
.map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?; |
||||
|
||||
let encoded_parts: Vec<Vec<EncodePart>> = result |
||||
.iter() |
||||
.map(|split| { |
||||
split |
||||
.iter() |
||||
.map(|part| EncodePart { |
||||
value: part.value, |
||||
words: part.words.clone(), |
||||
}) |
||||
.collect() |
||||
}) |
||||
.collect(); |
||||
|
||||
Ok(Json(EncodeResponse { |
||||
input, |
||||
dict: dict_name, |
||||
result: encoded_parts, |
||||
})) |
||||
} |
||||
|
||||
pub async fn decode_handler( |
||||
State(state): State<Arc<AppState>>, |
||||
Path(input): Path<String>, |
||||
Query(_params): Query<DecodeQuery>, |
||||
) -> Result<Json<DecodeResponse>, ErrorResponse> { |
||||
let decoder = state |
||||
.container |
||||
.create_decoder() |
||||
.map_err(|e| anyhow::anyhow!("Failed to create decoder: {}", e))?; |
||||
let result = decoder |
||||
.decode(&input) |
||||
.map_err(|e| anyhow::anyhow!("Failed to decode: {}", e))?; |
||||
|
||||
Ok(Json(DecodeResponse { |
||||
input, |
||||
result: result.as_str().to_string(), |
||||
})) |
||||
} |
||||
|
||||
pub fn routes() -> Router<Arc<AppState>> { |
||||
Router::new() |
||||
.route("/encode/major_pl/{input}", get(encode_handler)) |
||||
.route("/decode/major_pl/{input}", get(decode_handler)) |
||||
} |
||||
@ -1,23 +1,15 @@
|
||||
use axum::{ |
||||
Router, |
||||
routing::{get, post}, |
||||
}; |
||||
use axum::Router; |
||||
use std::sync::Arc; |
||||
use tower_http::{cors::CorsLayer, trace::TraceLayer}; |
||||
|
||||
mod handlers; |
||||
mod responses; |
||||
mod state; |
||||
use crate::api; |
||||
use crate::state::AppState; |
||||
|
||||
pub use state::AppState; |
||||
pub async fn create_router() -> anyhow::Result<Router> { |
||||
let state = Arc::new(AppState::new().await?); |
||||
|
||||
pub fn create_router() -> Router { |
||||
let state = Arc::new(AppState::new()); |
||||
|
||||
Router::new() |
||||
.route("/api/echo", post(handlers::echo_handler)) |
||||
.route("/api/version", get(handlers::version_handler)) |
||||
Ok(api::routes() |
||||
.with_state(state) |
||||
.layer(TraceLayer::new_for_http()) |
||||
.layer(CorsLayer::permissive()) |
||||
.layer(CorsLayer::permissive())) |
||||
} |
||||
|
||||
@ -1,25 +0,0 @@
|
||||
use axum::{Json, extract::State}; |
||||
use chrono::Utc; |
||||
use serde_json::Value; |
||||
use std::sync::Arc; |
||||
|
||||
use super::responses::{EchoResponse, ErrorResponse, VersionResponse}; |
||||
use super::state::AppState; |
||||
|
||||
pub async fn echo_handler( |
||||
State(_state): State<Arc<AppState>>, |
||||
Json(payload): Json<Value>, |
||||
) -> Result<Json<EchoResponse>, ErrorResponse> { |
||||
let response = EchoResponse { |
||||
data: payload, |
||||
timestamp: Utc::now().to_rfc3339(), |
||||
}; |
||||
Ok(Json(response)) |
||||
} |
||||
|
||||
pub async fn version_handler(State(state): State<Arc<AppState>>) -> Json<VersionResponse> { |
||||
Json(VersionResponse { |
||||
name: state.name.clone(), |
||||
version: state.version.clone(), |
||||
}) |
||||
} |
||||
@ -1,34 +0,0 @@
|
||||
use axum::{Json, http::StatusCode, response::IntoResponse}; |
||||
use serde::Serialize; |
||||
use serde_json::Value; |
||||
|
||||
#[derive(Debug, Serialize)] |
||||
pub struct EchoResponse { |
||||
pub data: Value, |
||||
pub timestamp: String, |
||||
} |
||||
|
||||
#[derive(Debug, Serialize)] |
||||
pub struct VersionResponse { |
||||
pub name: String, |
||||
pub version: String, |
||||
} |
||||
|
||||
#[derive(Debug, Serialize)] |
||||
pub struct ErrorResponse { |
||||
pub error: String, |
||||
} |
||||
|
||||
impl IntoResponse for ErrorResponse { |
||||
fn into_response(self) -> axum::response::Response { |
||||
(StatusCode::INTERNAL_SERVER_ERROR, Json(self)).into_response() |
||||
} |
||||
} |
||||
|
||||
impl<E: std::error::Error> From<E> for ErrorResponse { |
||||
fn from(err: E) -> Self { |
||||
Self { |
||||
error: err.to_string(), |
||||
} |
||||
} |
||||
} |
||||
@ -1,23 +1,21 @@
|
||||
pub const APP_NAME: &str = env!("CARGO_PKG_NAME"); |
||||
pub const APP_VERSION: &str = env!("CARGO_PKG_VERSION"); |
||||
|
||||
use crate::container::Container; |
||||
|
||||
#[derive(Clone)] |
||||
pub struct AppState { |
||||
pub name: String, |
||||
pub version: String, |
||||
pub container: Container, |
||||
} |
||||
|
||||
impl AppState { |
||||
pub fn new() -> Self { |
||||
Self { |
||||
pub async fn new() -> anyhow::Result<Self> { |
||||
Ok(Self { |
||||
name: APP_NAME.to_string(), |
||||
version: APP_VERSION.to_string(), |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl Default for AppState { |
||||
fn default() -> Self { |
||||
Self::new() |
||||
container: Container::new().await?, |
||||
}) |
||||
} |
||||
} |
||||
@ -0,0 +1,77 @@
|
||||
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 show_counts: bool, |
||||
} |
||||
|
||||
#[derive(ClapArgs, Debug, Clone)] |
||||
pub struct ListDictsCmd { |
||||
#[arg(short, long, help = defaults::HELP_LIST_DICTS_COUNTS)] |
||||
pub show_counts: bool, |
||||
} |
||||
|
||||
impl Configurable for ListDictsCmd { |
||||
fn apply_defaults( |
||||
&self, |
||||
builder: ConfigBuilder<DefaultState>, |
||||
) -> Result<ConfigBuilder<DefaultState>> { |
||||
builder |
||||
.set_default("list_dicts.show_counts", defaults::SHOW_COUNTS) |
||||
.map_err(Into::into) |
||||
} |
||||
|
||||
fn apply_overrides( |
||||
&self, |
||||
builder: ConfigBuilder<DefaultState>, |
||||
) -> Result<ConfigBuilder<DefaultState>> { |
||||
builder |
||||
.set_override("list_dicts.show_counts", self.show_counts) |
||||
.map_err(Into::into) |
||||
} |
||||
} |
||||
|
||||
#[async_trait] |
||||
impl Executable for ListDictsCmd { |
||||
async fn execute(&self, config: &AppConfig, container: &Container) -> Result<()> { |
||||
let config = config |
||||
.list_dicts |
||||
.as_ref() |
||||
.expect("ListDicts config not set"); |
||||
|
||||
let repo = container.create_dict_repo("default").await?; |
||||
|
||||
let dicts = repo.fetch_dicts().await?; |
||||
|
||||
if dicts.is_empty() { |
||||
println!("No dictionaries found."); |
||||
return Ok(()); |
||||
} |
||||
|
||||
println!("Dictionaries:"); |
||||
for dict_name in &dicts { |
||||
if config.show_counts { |
||||
let count_repo = container.create_dict_repo(dict_name).await?; |
||||
let count = count_repo.count_entries().await?; |
||||
println!(" - {} ({} entries)", dict_name, count); |
||||
} else { |
||||
println!(" - {}", dict_name); |
||||
} |
||||
} |
||||
|
||||
Ok(()) |
||||
} |
||||
} |
||||
|
||||
mod defaults { |
||||
use const_format::formatcp; |
||||
pub const SHOW_COUNTS: bool = false; |
||||
pub const HELP_LIST_DICTS_COUNTS: &str = formatcp!("Show entry counts for each dictionary"); |
||||
} |
||||
@ -1,6 +1,80 @@
|
||||
mod dict_importer; |
||||
mod infrastructure; |
||||
|
||||
use futures::stream::BoxStream; |
||||
|
||||
use crate::common::errors::RepositoryError; |
||||
|
||||
pub use self::dict_importer::DictImporter; |
||||
pub use self::infrastructure::json_file_dict_source::JsonFileDictSource; |
||||
pub use self::infrastructure::sqlite_dict_repository::SqliteDictRepository; |
||||
|
||||
use std::collections::HashMap; |
||||
|
||||
pub type DictEntryId = u64; |
||||
|
||||
#[derive(Debug, Clone, PartialEq)] |
||||
pub struct DictEntry { |
||||
pub id: Option<DictEntryId>, |
||||
pub text: String, |
||||
pub metadata: HashMap<String, String>, |
||||
} |
||||
|
||||
impl DictEntry { |
||||
pub fn new(id: Option<DictEntryId>, text: String) -> Self { |
||||
DictEntry { |
||||
id, |
||||
text, |
||||
metadata: HashMap::new(), |
||||
} |
||||
} |
||||
} |
||||
|
||||
#[derive(Debug, Clone)] |
||||
pub struct Dict { |
||||
pub name: String, |
||||
pub entries: HashMap<DictEntryId, DictEntry>, |
||||
} |
||||
|
||||
impl Dict { |
||||
pub fn new(name: String) -> Self { |
||||
Dict { |
||||
name, |
||||
entries: HashMap::new(), |
||||
} |
||||
} |
||||
|
||||
pub fn add_entry(&mut self, entry: DictEntry) { |
||||
self.entries.insert(entry.id.unwrap(), entry); |
||||
} |
||||
} |
||||
|
||||
#[async_trait::async_trait] |
||||
pub trait DictRepository: Send + Sync { |
||||
fn use_dict(&mut self, name: &str); |
||||
async fn create_dict(&self) -> Result<(), RepositoryError>; |
||||
|
||||
async fn fetch_dicts(&self) -> Result<Vec<String>, RepositoryError>; |
||||
|
||||
async fn count_entries(&self) -> Result<u64, RepositoryError>; |
||||
|
||||
/// "Upsert" logic:
|
||||
/// - If entry exists (by text), update metadata.
|
||||
/// - If not, insert new.
|
||||
/// - IDs are handled by the Database.
|
||||
async fn save_entries(&self, entries: &[DictEntry]) -> Result<(), RepositoryError>; |
||||
|
||||
/// Fetch a page of entries.
|
||||
async fn fetch_many(&self, limit: usize, offset: usize) -> Result<Dict, RepositoryError>; |
||||
|
||||
/// Returns a cold stream that fetches strings in chunks.
|
||||
/// The stream yields `Result<Vec<String>, RepositoryError>`.
|
||||
async fn stream_batches( |
||||
&self, |
||||
batch_size: usize, |
||||
) -> Result<BoxStream<'_, Result<Vec<String>, RepositoryError>>, RepositoryError>; |
||||
} |
||||
|
||||
pub trait DictSource { |
||||
fn next_entry(&mut self) -> Option<Result<DictEntry, anyhow::Error>>; |
||||
} |
||||
|
||||
Loading…
Reference in new issue