diff --git a/apps/app_api/Cargo.toml b/apps/app_api/Cargo.toml index 464aa4f..a66b64c 100644 --- a/apps/app_api/Cargo.toml +++ b/apps/app_api/Cargo.toml @@ -7,7 +7,6 @@ edition = "2024" # Internal applib = { path = "../../lib" } - # Runtime & Async tokio = { version = "1.48", features = ["full"] } anyhow = "1.0" @@ -21,4 +20,10 @@ serde_json = "1.0" toml = "0.9.8" clap = { version = "4.5", features = ["derive", "env"] } config = "0.15.19" -const_format = "0.2.35" +const_format = "0.2.35" +chrono = { version = "0.4", features = ["serde"] } + +# Web Framework +axum = "0.8" +tower = "0.5" +tower-http = { version = "0.6", features = ["trace", "cors"] } diff --git a/apps/app_api/config.toml.example b/apps/app_api/config.toml.example new file mode 100644 index 0000000..0309283 --- /dev/null +++ b/apps/app_api/config.toml.example @@ -0,0 +1,5 @@ +[listen] +host = "0.0.0.0" +port = 3000 + +log_level = "info" \ No newline at end of file diff --git a/apps/app_api/src/commands/listen.rs b/apps/app_api/src/commands/listen.rs index bb9a83f..431004d 100644 --- a/apps/app_api/src/commands/listen.rs +++ b/apps/app_api/src/commands/listen.rs @@ -1,12 +1,15 @@ use crate::commands::{ClapArgs, Configurable, Executable}; use crate::config::AppConfig; use crate::container::Container; +use crate::router; use anyhow::Result; use async_trait::async_trait; use config::ConfigBuilder; use config::builder::DefaultState; use serde::Deserialize; +use tokio::net::TcpListener; +use tracing::info; #[derive(Debug, Deserialize, Clone)] pub struct Config { @@ -16,7 +19,7 @@ pub struct Config { #[derive(ClapArgs, Debug, Clone)] pub struct ListenCmd { - #[arg(short, long, help = defaults::HELP_LISTEN_HOST)] + #[arg(short = 'H', long, help = defaults::HELP_LISTEN_HOST)] pub host: Option, #[arg(short, long, help = defaults::HELP_LISTEN_PORT)] @@ -52,18 +55,53 @@ impl Configurable for ListenCmd { #[async_trait] impl Executable for ListenCmd { - async fn execute(&self, config: &AppConfig, container: &Container) -> Result<()> { - let config = config + async fn execute(&self, config: &AppConfig, _container: &Container) -> Result<()> { + let listen_config = config .listen .as_ref() - .ok_or_else(|| anyhow::anyhow!("Decoder config missing"))?; + .ok_or_else(|| anyhow::anyhow!("Listen config missing"))?; - // TODO: start axum server + let app = router::create_router(); + let addr = format!("{}:{}", listen_config.host, listen_config.port); + let listener = TcpListener::bind(&addr).await?; + + info!("Starting server on {}", addr); + + axum::serve(listener, app) + .with_graceful_shutdown(shutdown_signal()) + .await?; + + info!("Server shut down gracefully"); Ok(()) } } +async fn shutdown_signal() { + let ctrl_c = async { + tokio::signal::ctrl_c() + .await + .expect("Failed to install Ctrl+C handler"); + }; + + #[cfg(unix)] + let terminate = async { + tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) + .expect("Failed to install signal handler") + .recv() + .await; + info!("Received SIGTERM signal"); + }; + + #[cfg(not(unix))] + let terminate = std::future::pending::<()>(); + + tokio::select! { + _ = ctrl_c => {}, + _ = terminate => {}, + } +} + mod defaults { use const_format::formatcp; pub const LISTEN_HOST: &str = "0.0.0.0"; diff --git a/apps/app_api/src/container.rs b/apps/app_api/src/container.rs index d021855..981a0b3 100644 --- a/apps/app_api/src/container.rs +++ b/apps/app_api/src/container.rs @@ -1,8 +1,8 @@ -use std::sync::Arc; +// use std::sync::Arc; -use applib::DictImporter; -use applib::DictRepository; -use applib::SqliteDictRepository; +// use applib::DictImporter; +// use applib::DictRepository; +// use applib::SqliteDictRepository; // use applib::SystemDecoder; // use applib::SystemEncoder; // use applib::sys_major as major; @@ -15,17 +15,17 @@ impl Container { 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_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)) - } + // 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/apps/app_api/src/main.rs b/apps/app_api/src/main.rs index 377c91b..6ec4d00 100644 --- a/apps/app_api/src/main.rs +++ b/apps/app_api/src/main.rs @@ -2,6 +2,7 @@ mod app; mod commands; mod config; mod container; +mod router; use anyhow::Result; use app::Application; diff --git a/apps/app_api/src/router.rs b/apps/app_api/src/router.rs new file mode 100644 index 0000000..16faf29 --- /dev/null +++ b/apps/app_api/src/router.rs @@ -0,0 +1,23 @@ +use axum::{ + Router, + routing::{get, post}, +}; +use std::sync::Arc; +use tower_http::{cors::CorsLayer, trace::TraceLayer}; + +mod handlers; +mod responses; +mod state; + +pub use state::AppState; + +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)) + .with_state(state) + .layer(TraceLayer::new_for_http()) + .layer(CorsLayer::permissive()) +} diff --git a/apps/app_api/src/router/handlers.rs b/apps/app_api/src/router/handlers.rs new file mode 100644 index 0000000..ea79e21 --- /dev/null +++ b/apps/app_api/src/router/handlers.rs @@ -0,0 +1,25 @@ +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>, + Json(payload): Json, +) -> Result, ErrorResponse> { + let response = EchoResponse { + data: payload, + timestamp: Utc::now().to_rfc3339(), + }; + Ok(Json(response)) +} + +pub async fn version_handler(State(state): State>) -> Json { + Json(VersionResponse { + name: state.name.clone(), + version: state.version.clone(), + }) +} diff --git a/apps/app_api/src/router/responses.rs b/apps/app_api/src/router/responses.rs new file mode 100644 index 0000000..e2ab6cd --- /dev/null +++ b/apps/app_api/src/router/responses.rs @@ -0,0 +1,34 @@ +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 From for ErrorResponse { + fn from(err: E) -> Self { + Self { + error: err.to_string(), + } + } +} diff --git a/apps/app_api/src/router/state.rs b/apps/app_api/src/router/state.rs new file mode 100644 index 0000000..5a2e6b4 --- /dev/null +++ b/apps/app_api/src/router/state.rs @@ -0,0 +1,23 @@ +pub const APP_NAME: &str = env!("CARGO_PKG_NAME"); +pub const APP_VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[derive(Clone)] +pub struct AppState { + pub name: String, + pub version: String, +} + +impl AppState { + pub fn new() -> Self { + Self { + name: APP_NAME.to_string(), + version: APP_VERSION.to_string(), + } + } +} + +impl Default for AppState { + fn default() -> Self { + Self::new() + } +}