summaryrefslogtreecommitdiffstats
path: root/crates
diff options
context:
space:
mode:
authorrtkay123 <dev@kanjala.com>2025-11-23 12:38:49 +0200
committerrtkay123 <dev@kanjala.com>2025-11-23 12:38:49 +0200
commitae72e4f8d4ccb6d5ed71e17d6b2ffb0ac8876e0a (patch)
tree89e4c44a9e3e0c6cd32976fdc69172d42375fcf2 /crates
parent432a5061c0b910821635825021a37f798e0ce26c (diff)
downloadsellershut-ae72e4f8d4ccb6d5ed71e17d6b2ffb0ac8876e0a.tar.bz2
sellershut-ae72e4f8d4ccb6d5ed71e17d6b2ffb0ac8876e0a.zip
feat: apidoc
Diffstat (limited to 'crates')
-rw-r--r--crates/sellershut/Cargo.toml15
-rw-r--r--crates/sellershut/src/config.rs (renamed from crates/sellershut/src/config/mod.rs)14
-rw-r--r--crates/sellershut/src/logging.rs19
-rw-r--r--crates/sellershut/src/main.rs42
-rw-r--r--crates/sellershut/src/server.rs42
-rw-r--r--crates/sellershut/src/server/doc.rs11
-rw-r--r--crates/sellershut/src/server/middleware.rs2
-rw-r--r--crates/sellershut/src/server/middleware/request_id.rs11
-rw-r--r--crates/sellershut/src/server/middleware/timeout.rs15
-rw-r--r--crates/sellershut/src/server/routes.rs15
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")
+}