diff options
| author | rtkay123 <dev@kanjala.com> | 2025-11-23 12:38:49 +0200 |
|---|---|---|
| committer | rtkay123 <dev@kanjala.com> | 2025-11-23 12:38:49 +0200 |
| commit | ae72e4f8d4ccb6d5ed71e17d6b2ffb0ac8876e0a (patch) | |
| tree | 89e4c44a9e3e0c6cd32976fdc69172d42375fcf2 /crates | |
| parent | 432a5061c0b910821635825021a37f798e0ce26c (diff) | |
| download | sellershut-ae72e4f8d4ccb6d5ed71e17d6b2ffb0ac8876e0a.tar.bz2 sellershut-ae72e4f8d4ccb6d5ed71e17d6b2ffb0ac8876e0a.zip | |
feat: apidoc
Diffstat (limited to 'crates')
| -rw-r--r-- | crates/sellershut/Cargo.toml | 15 | ||||
| -rw-r--r-- | crates/sellershut/src/config.rs (renamed from crates/sellershut/src/config/mod.rs) | 14 | ||||
| -rw-r--r-- | crates/sellershut/src/logging.rs | 19 | ||||
| -rw-r--r-- | crates/sellershut/src/main.rs | 42 | ||||
| -rw-r--r-- | crates/sellershut/src/server.rs | 42 | ||||
| -rw-r--r-- | crates/sellershut/src/server/doc.rs | 11 | ||||
| -rw-r--r-- | crates/sellershut/src/server/middleware.rs | 2 | ||||
| -rw-r--r-- | crates/sellershut/src/server/middleware/request_id.rs | 11 | ||||
| -rw-r--r-- | crates/sellershut/src/server/middleware/timeout.rs | 15 | ||||
| -rw-r--r-- | crates/sellershut/src/server/routes.rs | 15 |
10 files changed, 150 insertions, 36 deletions
diff --git a/crates/sellershut/Cargo.toml b/crates/sellershut/Cargo.toml index 9fcf378..aee606e 100644 --- a/crates/sellershut/Cargo.toml +++ b/crates/sellershut/Cargo.toml @@ -13,6 +13,19 @@ anyhow = "1.0.100" axum = { version = "0.8.7", features = ["macros"] } clap = { version = "4.5.53", features = ["derive", "env"] } tokio = { version = "1.48.0", features = ["macros", "rt-multi-thread", "signal"] } -tower-http = { version = "0.6.6", features = ["trace", "timeout"] } +tower = "0.5.2" +tower-http = { version = "0.6.6", features = ["propagate-header", "request-id", "trace", "timeout"] } tracing.workspace = true tracing-subscriber = { version = "0.3.20", features = ["env-filter"] } +utoipa = "5.4.0" +utoipa-axum = "0.2.0" +utoipa-rapidoc = { version = "6.0.0", features = ["axum"], optional = true } +utoipa-redoc = { version = "6.0.0", optional = true, features = ["axum"] } +utoipa-scalar = { version = "0.3.0", features = ["axum"], optional = true } +utoipa-swagger-ui = { version = "9.0.2", features = ["axum"], optional = true } + +[features] +redoc = ["dep:utoipa-redoc"] +rapidoc = ["dep:utoipa-rapidoc"] +scalar = ["dep:utoipa-scalar"] +swagger-ui = ["dep:utoipa-swagger-ui"] diff --git a/crates/sellershut/src/config/mod.rs b/crates/sellershut/src/config.rs index 9ce7b2d..65383a6 100644 --- a/crates/sellershut/src/config/mod.rs +++ b/crates/sellershut/src/config.rs @@ -5,20 +5,26 @@ use std::path::PathBuf; use clap::Parser; use logging::LogLevel; +pub const LOG_LEVEL: &str = "LOG_LEVEL"; + #[derive(Parser, Debug)] #[command(version, about, long_about = None)] pub struct Cli { /// Sets the port the server listens on - #[arg(default_value_t = 2210, env = "PORT")] - port: u16, + #[arg(short, long, default_value_t = 2210, env = "PORT")] + pub port: u16, - /// Sets the port the server listens on - #[arg(short, long, value_enum, env = "LOG_LEVEL", default_value_t = LogLevel::Debug)] + /// Sets the application log level + #[arg(short, long, value_enum, env = LOG_LEVEL, default_value_t = LogLevel::Debug)] log_level: LogLevel, /// Sets a custom config file #[arg(short, long, value_name = "FILE")] config: Option<PathBuf>, + + /// Request timeout duration (in seconds) + #[arg(short, long, default_value_t = 10, env = "TIMEOUT_DURATION")] + pub timeout_duration: u64, } impl Cli { diff --git a/crates/sellershut/src/logging.rs b/crates/sellershut/src/logging.rs new file mode 100644 index 0000000..89b8686 --- /dev/null +++ b/crates/sellershut/src/logging.rs @@ -0,0 +1,19 @@ +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; + +use crate::config::{Cli, LOG_LEVEL}; + +pub fn initialise_logging(config: &Cli) { + tracing_subscriber::registry() + .with( + tracing_subscriber::EnvFilter::try_from_env(LOG_LEVEL).unwrap_or_else(|_| { + format!( + "{}={},tower_http=debug,axum=trace", + env!("CARGO_CRATE_NAME"), + config.log_level() + ) + .into() + }), + ) + .with(tracing_subscriber::fmt::layer()) + .init(); +} diff --git a/crates/sellershut/src/main.rs b/crates/sellershut/src/main.rs index 9736986..07e6193 100644 --- a/crates/sellershut/src/main.rs +++ b/crates/sellershut/src/main.rs @@ -1,45 +1,25 @@ mod config; +mod logging; +mod server; -use std::time::Duration; +use std::net::{Ipv6Addr, SocketAddr}; -use axum::{Router, routing::get}; use clap::Parser; use tokio::{net::TcpListener, signal}; -use tower_http::{timeout::TimeoutLayer, trace::TraceLayer}; -use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; +use tracing::info; + +use crate::logging::initialise_logging; #[tokio::main] async fn main() -> anyhow::Result<()> { let config = config::Cli::parse(); - dbg!(&config); - - tracing_subscriber::registry() - .with( - tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| { - format!( - "{}={},tower_http=debug,axum=trace", - env!("CARGO_CRATE_NAME"), - config.log_level() - ) - .into() - }), - ) - .with(tracing_subscriber::fmt::layer().without_time()) - .init(); + initialise_logging(&config); - // Create a regular axum app. - let app = Router::new() - .route("/slow", get(|| tokio::time::sleep(Duration::from_secs(5)))) - .route("/forever", get(std::future::pending::<()>)) - .layer(( - TraceLayer::new_for_http(), - // Graceful shutdown will wait for outstanding requests to complete. Add a timeout so - // requests don't hang forever. - TimeoutLayer::new(Duration::from_secs(10)), - )); + let app = server::router(&config); - // Create a `TcpListener` using tokio. - let listener = TcpListener::bind("0.0.0.0:3000").await?; + let addr = SocketAddr::from((Ipv6Addr::UNSPECIFIED, config.port)); + info!(port = addr.port(), "starting server"); + let listener = TcpListener::bind(addr).await?; // Run the server with graceful shutdown axum::serve(listener, app) diff --git a/crates/sellershut/src/server.rs b/crates/sellershut/src/server.rs new file mode 100644 index 0000000..cee6146 --- /dev/null +++ b/crates/sellershut/src/server.rs @@ -0,0 +1,42 @@ +mod doc; +mod middleware; +mod routes; + +use axum::Router; +use utoipa::OpenApi as _; +use utoipa_axum::{router::OpenApiRouter, routes}; + +#[cfg(feature = "redoc")] +use utoipa_redoc::Servable as _; +#[cfg(feature = "scalar")] +use utoipa_scalar::Servable as _; + +use crate::{config::Cli, server::doc::ApiDoc}; + +pub fn router(config: &Cli) -> Router { + let (router, _api) = OpenApiRouter::with_openapi(ApiDoc::openapi()) + .routes(routes!(routes::health_check)) + .split_for_parts(); + + #[cfg(feature = "swagger-ui")] + let router = router.merge( + utoipa_swagger_ui::SwaggerUi::new("/swagger-ui") + .url("/api-docs/swaggerdoc.json", _api.clone()), + ); + + #[cfg(feature = "redoc")] + let router = router.merge(utoipa_redoc::Redoc::with_url("/redoc", _api.clone())); + + #[cfg(feature = "rapidoc")] + let router = router.merge( + utoipa_rapidoc::RapiDoc::with_openapi("/api-docs/rapidoc.json", _api.clone()) + .path("/rapidoc"), + ); + + #[cfg(feature = "scalar")] + let router = router.merge(utoipa_scalar::Scalar::with_url("/scalar", _api)); + + let router = middleware::timeout::apply(router, config.timeout_duration); + + middleware::request_id::apply(router) +} diff --git a/crates/sellershut/src/server/doc.rs b/crates/sellershut/src/server/doc.rs new file mode 100644 index 0000000..11b561e --- /dev/null +++ b/crates/sellershut/src/server/doc.rs @@ -0,0 +1,11 @@ +use utoipa::OpenApi; + +pub(super) const HEALTH: &str = "HEALTH"; + +#[derive(OpenApi)] +#[openapi( + tags( + (name = HEALTH, description = "Check API health"), + ) +)] +pub struct ApiDoc; diff --git a/crates/sellershut/src/server/middleware.rs b/crates/sellershut/src/server/middleware.rs new file mode 100644 index 0000000..0c44376 --- /dev/null +++ b/crates/sellershut/src/server/middleware.rs @@ -0,0 +1,2 @@ +pub(super) mod request_id; +pub(super) mod timeout; diff --git a/crates/sellershut/src/server/middleware/request_id.rs b/crates/sellershut/src/server/middleware/request_id.rs new file mode 100644 index 0000000..cce6898 --- /dev/null +++ b/crates/sellershut/src/server/middleware/request_id.rs @@ -0,0 +1,11 @@ +use axum::{Router, http::HeaderName}; +use tower_http::propagate_header::PropagateHeaderLayer; +use tracing::trace; + +pub fn apply(router: Router) -> Router { + trace!("applying x-request-id middleware"); + + router.layer(PropagateHeaderLayer::new(HeaderName::from_static( + "x-request-id", + ))) +} diff --git a/crates/sellershut/src/server/middleware/timeout.rs b/crates/sellershut/src/server/middleware/timeout.rs new file mode 100644 index 0000000..ff39c6a --- /dev/null +++ b/crates/sellershut/src/server/middleware/timeout.rs @@ -0,0 +1,15 @@ +use std::time::Duration; + +use axum::Router; +use tower_http::{timeout::TimeoutLayer, trace::TraceLayer}; +use tracing::trace; + +pub fn apply(router: Router, duration: u64) -> Router { + trace!(seconds = duration, "applying timeout middleware"); + router.layer(( + TraceLayer::new_for_http(), + // Graceful shutdown will wait for outstanding requests to complete. Add a timeout so + // requests don't hang forever. + TimeoutLayer::new(Duration::from_secs(duration)), + )) +} diff --git a/crates/sellershut/src/server/routes.rs b/crates/sellershut/src/server/routes.rs new file mode 100644 index 0000000..e5d1031 --- /dev/null +++ b/crates/sellershut/src/server/routes.rs @@ -0,0 +1,15 @@ +/// Get health of the API. +#[utoipa::path( + method(get), + path = "/", + tag = super::doc::HEALTH, + responses( + (status = OK, description = "Success", body = str, content_type = "text/plain") + ) +)] +pub async fn health_check() -> impl axum::response::IntoResponse { + let name = env!("CARGO_PKG_NAME"); + let ver = env!("CARGO_PKG_VERSION"); + + format!("{name} v{ver} is live") +} |
