[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]
|
||||
members = [
|
||||
"crates/app",
|
||||
"crates/api",
|
||||
"crates/module-api",
|
||||
"crates/module-host",
|
||||
"crates/modules/summarizer",
|
||||
"crates/server",
|
||||
"crates/cli",
|
||||
]
|
||||
resolver = "2"
|
||||
resolver = "3"
|
||||
|
||||
[workspace.package]
|
||||
edition = "2024"
|
||||
version = "0.1.0"
|
||||
rust-version = "1.89"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0"
|
||||
tokio = "1"
|
||||
axum = "0.8.4"
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
sqlx = "0.8"
|
||||
dotenv = "0.15"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = "0.3"
|
||||
anyhow = "1.0.99"
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_json = "1.0.142"
|
||||
tokio = { version = "1.47.1", features = ["rt-multi-thread", "macros", "signal"] }
|
||||
libloading = "0.8.8"
|
||||
tracing = "0.1.41"
|
||||
once_cell = "1.21.3"
|
||||
toml = "0.9.5"
|
||||
unicode-segmentation = "1.12.0"
|
||||
axum = "0.8.4"
|
||||
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"
|
||||
num_cpus = "1.17.0"
|
||||
unicode-segmentation = "1.12.0"
|
||||
readability = "0.3.0"
|
||||
tracing-subscriber = { version = "0.3.19", features = ["env-filter", "fmt"] }
|
||||
scraper = "0.23.1"
|
||||
libloading = "0.8"
|
||||
async-trait = "0.1"
|
||||
once_cell = "1.19"
|
||||
num_cpus = "1.16"
|
||||
dotenv = "0.15.0"
|
||||
|
||||
# Dev-only deps centralized (optional)
|
||||
tokio-test = "0.4"
|
||||
axum-test = "17.3"
|
||||
# dev/test utilities in the workspace
|
||||
tokio-test = "0.4.4"
|
||||
axum-test = "17.3.0"
|
||||
|
@@ -1,28 +1,16 @@
|
||||
[package]
|
||||
name = "owly-news-api"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
name = "api"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
axum = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
sqlx = { workspace = true, features = ["runtime-tokio", "tls-native-tls", "sqlite", "macros", "migrate", "chrono", "json"] }
|
||||
dotenv = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter", "json"] }
|
||||
once_cell = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
unicode-segmentation = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
hex = { workspace = true }
|
||||
readability = { workspace = true }
|
||||
scraper = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
async-trait = "0.1.89"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio-test = { workspace = true }
|
||||
axum-test = { workspace = true }
|
||||
[features]
|
||||
default = []
|
||||
|
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;
|
||||
pub use api::*;
|
||||
//! API-first core: shared types, DTOs, service traits, configuration.
|
||||
|
||||
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