[feat] introduced server
and cli
crates with foundational HTTP server and CLI implementation, including routing, health check, and configuration setup
This commit is contained in:
1385
backend-rust/Cargo.lock
generated
1385
backend-rust/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,38 +1,36 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"crates/app",
|
|
||||||
"crates/api",
|
"crates/api",
|
||||||
"crates/module-api",
|
"crates/server",
|
||||||
"crates/module-host",
|
"crates/cli",
|
||||||
"crates/modules/summarizer",
|
|
||||||
]
|
]
|
||||||
resolver = "2"
|
resolver = "3"
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
rust-version = "1.89"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
anyhow = "1.0"
|
anyhow = "1.0.99"
|
||||||
tokio = "1"
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
axum = "0.8.4"
|
serde_json = "1.0.142"
|
||||||
serde = "1.0"
|
tokio = { version = "1.47.1", features = ["rt-multi-thread", "macros", "signal"] }
|
||||||
serde_json = "1.0"
|
libloading = "0.8.8"
|
||||||
sqlx = "0.8"
|
tracing = "0.1.41"
|
||||||
dotenv = "0.15"
|
once_cell = "1.21.3"
|
||||||
tracing = "0.1"
|
|
||||||
tracing-subscriber = "0.3"
|
|
||||||
toml = "0.9.5"
|
toml = "0.9.5"
|
||||||
unicode-segmentation = "1.12.0"
|
axum = "0.8.4"
|
||||||
sha2 = "0.10.9"
|
sha2 = "0.10.9"
|
||||||
|
sqlx = { version = "0.8.6", default-features = false, features = ["runtime-tokio-rustls", "macros", "postgres", "uuid", "chrono", "migrate"] }
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
|
num_cpus = "1.17.0"
|
||||||
|
unicode-segmentation = "1.12.0"
|
||||||
readability = "0.3.0"
|
readability = "0.3.0"
|
||||||
|
tracing-subscriber = { version = "0.3.19", features = ["env-filter", "fmt"] }
|
||||||
scraper = "0.23.1"
|
scraper = "0.23.1"
|
||||||
libloading = "0.8"
|
dotenv = "0.15.0"
|
||||||
async-trait = "0.1"
|
|
||||||
once_cell = "1.19"
|
|
||||||
num_cpus = "1.16"
|
|
||||||
|
|
||||||
# Dev-only deps centralized (optional)
|
# dev/test utilities in the workspace
|
||||||
tokio-test = "0.4"
|
tokio-test = "0.4.4"
|
||||||
axum-test = "17.3"
|
axum-test = "17.3.0"
|
||||||
|
@@ -1,28 +1,16 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "owly-news-api"
|
name = "api"
|
||||||
version.workspace = true
|
version = "0.1.0"
|
||||||
edition.workspace = true
|
edition = "2024"
|
||||||
|
|
||||||
[lib]
|
|
||||||
path = "src/lib.rs"
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
tokio = { workspace = true, features = ["full"] }
|
serde = { workspace = true }
|
||||||
axum = { workspace = true }
|
|
||||||
serde = { workspace = true, features = ["derive"] }
|
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
sqlx = { workspace = true, features = ["runtime-tokio", "tls-native-tls", "sqlite", "macros", "migrate", "chrono", "json"] }
|
once_cell = { workspace = true }
|
||||||
dotenv = { workspace = true }
|
|
||||||
tracing = { workspace = true }
|
|
||||||
tracing-subscriber = { workspace = true, features = ["env-filter", "json"] }
|
|
||||||
toml = { workspace = true }
|
toml = { workspace = true }
|
||||||
unicode-segmentation = { workspace = true }
|
tracing = { workspace = true }
|
||||||
sha2 = { workspace = true }
|
async-trait = "0.1.89"
|
||||||
hex = { workspace = true }
|
|
||||||
readability = { workspace = true }
|
|
||||||
scraper = { workspace = true }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[features]
|
||||||
tokio-test = { workspace = true }
|
default = []
|
||||||
axum-test = { workspace = true }
|
|
||||||
|
57
backend-rust/crates/api/src/config.rs
Normal file
57
backend-rust/crates/api/src/config.rs
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct Cli {
|
||||||
|
#[serde(default)]
|
||||||
|
pub default_output: DefaultOutput,
|
||||||
|
#[serde(default = "Cli::default_pager_command")]
|
||||||
|
pub pager_command: String,
|
||||||
|
#[serde(default = "Cli::default_show_progress")]
|
||||||
|
pub show_progress: bool,
|
||||||
|
#[serde(default = "Cli::default_auto_confirm_bulk")]
|
||||||
|
pub auto_confirm_bulk: bool,
|
||||||
|
#[serde(default = "Cli::default_show_geographic_hierarchy")]
|
||||||
|
pub show_geographic_hierarchy: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Cli {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
default_output: DefaultOutput::Table,
|
||||||
|
pager_command: Self::default_pager_command(),
|
||||||
|
show_progress: Self::default_show_progress(),
|
||||||
|
auto_confirm_bulk: Self::default_auto_confirm_bulk(),
|
||||||
|
show_geographic_hierarchy: Self::default_show_geographic_hierarchy(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum DefaultOutput {
|
||||||
|
Table,
|
||||||
|
Json,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DefaultOutput {
|
||||||
|
fn default() -> Self {
|
||||||
|
DefaultOutput::Table
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cli {
|
||||||
|
pub fn default_pager_command() -> String {
|
||||||
|
// Example default; customize as needed
|
||||||
|
"less -R".to_string()
|
||||||
|
}
|
||||||
|
pub fn default_show_progress() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
pub fn default_auto_confirm_bulk() -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
pub fn default_show_geographic_hierarchy() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
@@ -1,2 +1,5 @@
|
|||||||
pub mod api;
|
//! API-first core: shared types, DTOs, service traits, configuration.
|
||||||
pub use api::*;
|
|
||||||
|
pub mod config;
|
||||||
|
pub mod types;
|
||||||
|
pub mod services;
|
18
backend-rust/crates/api/src/services.rs
Normal file
18
backend-rust/crates/api/src/services.rs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
use crate::types::Health;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
// Implement your service traits here. Example:
|
||||||
|
#[async_trait]
|
||||||
|
pub trait HealthService: Send + Sync {
|
||||||
|
async fn health(&self) -> Health;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A trivial default implementation that can be used by server and tests.
|
||||||
|
pub struct DefaultHealthService;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl HealthService for DefaultHealthService {
|
||||||
|
async fn health(&self) -> Health {
|
||||||
|
Health { status: "ok".into() }
|
||||||
|
}
|
||||||
|
}
|
6
backend-rust/crates/api/src/types.rs
Normal file
6
backend-rust/crates/api/src/types.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Health {
|
||||||
|
pub status: String,
|
||||||
|
}
|
15
backend-rust/crates/cli/Cargo.toml
Normal file
15
backend-rust/crates/cli/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "cli"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = { workspace = true }
|
||||||
|
tokio = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
toml = { workspace = true }
|
||||||
|
dotenv = { workspace = true }
|
||||||
|
|
||||||
|
api = { path = "../api" }
|
||||||
|
server = { path = "../server" }
|
70
backend-rust/crates/cli/src/main.rs
Normal file
70
backend-rust/crates/cli/src/main.rs
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use api::config::Cli;
|
||||||
|
use dotenv::dotenv;
|
||||||
|
use std::{env, net::SocketAddr, str::FromStr};
|
||||||
|
use tokio::signal;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
dotenv().ok();
|
||||||
|
let args: Vec<String> = env::args().collect();
|
||||||
|
|
||||||
|
match args.get(1).map(|s| s.as_str()) {
|
||||||
|
Some("serve") => serve(args).await,
|
||||||
|
Some("print-config") => print_config(),
|
||||||
|
_ => {
|
||||||
|
print_help();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_help() {
|
||||||
|
eprintln!(
|
||||||
|
"Usage:
|
||||||
|
cli serve [--addr 0.0.0.0:8080]
|
||||||
|
cli print-config
|
||||||
|
|
||||||
|
Environment:
|
||||||
|
These may influence runtime behavior.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- 'serve' runs the HTTP server.
|
||||||
|
- 'print-config' prints the default CLI configuration in JSON."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn serve(args: Vec<String>) -> Result<()> {
|
||||||
|
// naive flag parse: look for "--addr host:port"
|
||||||
|
let mut addr: SocketAddr = SocketAddr::from_str("127.0.0.1:8080")?;
|
||||||
|
let mut i = 2;
|
||||||
|
while i + 1 < args.len() {
|
||||||
|
if args[i] == "--addr" {
|
||||||
|
addr = SocketAddr::from_str(&args[i + 1])?;
|
||||||
|
i += 2;
|
||||||
|
} else {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let server_task = tokio::spawn(async move { server::start_server(addr).await });
|
||||||
|
|
||||||
|
// graceful shutdown via Ctrl+C
|
||||||
|
tokio::select! {
|
||||||
|
res = server_task => {
|
||||||
|
res??;
|
||||||
|
}
|
||||||
|
_ = signal::ctrl_c() => {
|
||||||
|
eprintln!("Shutting down...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_config() -> Result<()> {
|
||||||
|
let cfg = Cli::default();
|
||||||
|
let json = serde_json::to_string_pretty(&cfg)?;
|
||||||
|
println!("{json}");
|
||||||
|
Ok(())
|
||||||
|
}
|
22
backend-rust/crates/server/Cargo.toml
Normal file
22
backend-rust/crates/server/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
[package]
|
||||||
|
name = "server"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = { workspace = true }
|
||||||
|
tokio = { workspace = true }
|
||||||
|
tracing = { workspace = true }
|
||||||
|
tracing-subscriber = { workspace = true }
|
||||||
|
axum = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
sqlx = { workspace = true }
|
||||||
|
dotenv = { workspace = true }
|
||||||
|
once_cell = { workspace = true }
|
||||||
|
|
||||||
|
api = { path = "../api" }
|
||||||
|
http = "1.3.1"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
58
backend-rust/crates/server/src/lib.rs
Normal file
58
backend-rust/crates/server/src/lib.rs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
use axum::{routing::get, Json, Router};
|
||||||
|
use std::{net::SocketAddr, sync::Arc};
|
||||||
|
use tokio::net::TcpListener;
|
||||||
|
use tracing::{info, level_filters::LevelFilter};
|
||||||
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
|
use api::services::{DefaultHealthService, HealthService};
|
||||||
|
use api::types::Health;
|
||||||
|
|
||||||
|
pub struct AppState {
|
||||||
|
pub health_service: Arc<dyn HealthService>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn build_router(state: Arc<AppState>) -> Router {
|
||||||
|
Router::new().route(
|
||||||
|
"/health",
|
||||||
|
get({
|
||||||
|
let state = state.clone();
|
||||||
|
move || health_handler(state.clone())
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn health_handler(state: Arc<AppState>) -> Json<Health> {
|
||||||
|
let res = state.health_service.health().await;
|
||||||
|
Json(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn start_server(addr: SocketAddr) -> anyhow::Result<()> {
|
||||||
|
init_tracing();
|
||||||
|
|
||||||
|
// TODO: initialize database pools and other infrastructure here.
|
||||||
|
// let pool = sqlx::PgPool::connect(&db_url).await?;
|
||||||
|
|
||||||
|
let state = Arc::new(AppState {
|
||||||
|
health_service: Arc::new(DefaultHealthService),
|
||||||
|
});
|
||||||
|
|
||||||
|
let app = build_router(state).await;
|
||||||
|
|
||||||
|
let listener = TcpListener::bind(addr).await?;
|
||||||
|
info!("HTTP server listening on http://{}", addr);
|
||||||
|
axum::serve(listener, app).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_tracing() {
|
||||||
|
let env_filter = EnvFilter::try_from_default_env()
|
||||||
|
.or_else(|_| EnvFilter::try_new("info"))
|
||||||
|
.unwrap()
|
||||||
|
.add_directive(LevelFilter::INFO.into());
|
||||||
|
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_env_filter(env_filter)
|
||||||
|
.with_target(true)
|
||||||
|
.compact()
|
||||||
|
.init();
|
||||||
|
}
|
22
backend-rust/crates/server/tests/health.rs
Normal file
22
backend-rust/crates/server/tests/health.rs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
use axum::Router;
|
||||||
|
use server::{build_router, AppState};
|
||||||
|
use api::services::DefaultHealthService;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn health_ok() {
|
||||||
|
let state = Arc::new(AppState {
|
||||||
|
health_service: Arc::new(DefaultHealthService),
|
||||||
|
});
|
||||||
|
|
||||||
|
let app: Router = build_router(state).await;
|
||||||
|
|
||||||
|
let req = http::Request::builder()
|
||||||
|
.uri("/health")
|
||||||
|
.body(axum::body::Body::empty())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let res = axum::http::Request::from(req);
|
||||||
|
let res = axum::http::Request::from(res);
|
||||||
|
let _ = app; // You can use axum-test to send requests if desired.
|
||||||
|
}
|
Reference in New Issue
Block a user