From d800cc4ac8afd368f0c514e9d7406080eaab0082 Mon Sep 17 00:00:00 2001 From: Isaac Leonard Date: Thu, 23 Oct 2025 01:37:41 +1100 Subject: [PATCH] Add automatic creation of open api documentation through the aide crate --- Cargo.toml | 4 +- src/app.rs | 51 ++++++++++++++------- src/boot.rs | 4 +- src/controller/app_routes.rs | 14 +++--- src/controller/describe.rs | 5 +- src/controller/format.rs | 6 +-- src/controller/health.rs | 14 +++--- src/controller/middleware/auth.rs | 5 ++ src/controller/middleware/catch_panic.rs | 4 +- src/controller/middleware/compression.rs | 4 +- src/controller/middleware/cors.rs | 4 +- src/controller/middleware/etag.rs | 7 ++- src/controller/middleware/fallback.rs | 5 +- src/controller/middleware/limit_payload.rs | 4 +- src/controller/middleware/logger.rs | 5 +- src/controller/middleware/mod.rs | 4 +- src/controller/middleware/powered_by.rs | 8 ++-- src/controller/middleware/remote_ip.rs | 4 +- src/controller/middleware/request_id.rs | 7 ++- src/controller/middleware/secure_headers.rs | 4 +- src/controller/middleware/static_assets.rs | 6 +-- src/controller/middleware/timeout.rs | 6 +-- src/controller/mod.rs | 12 +---- src/controller/ping.rs | 13 +++--- src/controller/routes.rs | 26 +++++++++-- src/initializers/extra_db.rs | 5 +- src/initializers/multi_db.rs | 5 +- src/prelude.rs | 6 ++- src/testing/request.rs | 3 ++ 29 files changed, 143 insertions(+), 102 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a0f0e66c7..6c51e5b1a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -128,7 +128,7 @@ axum-test = { version = "17.0.1", optional = true } chrono = { workspace = true } cfg-if = "1" -uuid = { version = "1.10.0", features = ["v4", "fast-rng"] } +uuid = { version = "1.10.0", features = ["v4", "fast-rng", "serde"] } # File Upload opendal = { version = "0.50.2", default-features = false, features = [ @@ -158,6 +158,8 @@ rusty-sidekiq = { version = "0.11.0", default-features = false, optional = true bb8 = { version = "0.8.1", optional = true } scraper = { version = "0.21.0", features = ["deterministic"], optional = true } +aide = { "version" = "0.14.0", "features" = ["axum", "axum-json"]} +schemars = { "version" = "0.8.21", "features" = ["uuid1"]} [workspace.dependencies] colored = { version = "2" } diff --git a/src/app.rs b/src/app.rs index f2658b8d9..678be5398 100644 --- a/src/app.rs +++ b/src/app.rs @@ -9,8 +9,8 @@ cfg_if::cfg_if! { } use std::{net::SocketAddr, sync::Arc}; +use aide::{axum::ApiRouter, openapi::OpenApi, transform::TransformOpenApi}; use async_trait::async_trait; -use axum::Router as AxumRouter; use crate::{ bgworker::{self, Queue}, @@ -54,6 +54,12 @@ pub struct AppContext { pub cache: Arc, } +impl std::fmt::Debug for AppContext { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("AppContext").finish() + } +} + /// A trait that defines hooks for customizing and extending the behavior of a /// web server application. /// @@ -110,7 +116,11 @@ pub trait Hooks: Send { /// /// # Returns /// A Result indicating success () or an error if the server fails to start. - async fn serve(app: AxumRouter, ctx: &AppContext, serve_params: &ServeParams) -> Result<()> { + async fn serve(app: ApiRouter, ctx: &AppContext, serve_params: &ServeParams) -> Result<()> { + aide::generate::on_error(|error| { + panic!("{error}"); + }); + let listener = tokio::net::TcpListener::bind(&format!( "{}:{}", serve_params.binding, serve_params.port @@ -118,16 +128,20 @@ pub trait Hooks: Send { .await?; let cloned_ctx = ctx.clone(); - axum::serve( - listener, - app.into_make_service_with_connect_info::(), - ) - .with_graceful_shutdown(async move { - shutdown_signal().await; - tracing::info!("shutting down..."); - Self::on_shutdown(&cloned_ctx).await; - }) - .await?; + + let mut api = OpenApi::default(); + let app = app + .finish_api_with(&mut api, Self::api_docs) + .into_make_service_with_connect_info::(); + std::fs::write("docs.json", serde_json::to_string_pretty(&api).unwrap()).unwrap(); + + axum::serve(listener, app) + .with_graceful_shutdown(async move { + shutdown_signal().await; + tracing::info!("shutting down..."); + Self::on_shutdown(&cloned_ctx).await; + }) + .await?; Ok(()) } @@ -157,8 +171,8 @@ pub trait Hooks: Send { /// /// # Errors /// Return an [`Result`] when the router could not be created - async fn before_routes(_ctx: &AppContext) -> Result> { - Ok(AxumRouter::new()) + async fn before_routes(_ctx: &AppContext) -> Result> { + Ok(ApiRouter::new()) } /// Invoke this function after the Loco routers have been constructed. This @@ -167,7 +181,7 @@ pub trait Hooks: Send { /// /// # Errors /// Axum router error - async fn after_routes(router: AxumRouter, _ctx: &AppContext) -> Result { + async fn after_routes(router: ApiRouter, _ctx: &AppContext) -> Result { Ok(router) } @@ -194,6 +208,11 @@ pub trait Hooks: Send { /// Defines the application's routing configuration. fn routes(_ctx: &AppContext) -> AppRoutes; + fn api_docs(api: TransformOpenApi) -> TransformOpenApi { + api.title(Self::app_name()) + .version(Self::app_version().as_str()) + } + // Provides the options to change Loco [`AppContext`] after initialization. async fn after_context(ctx: AppContext) -> Result { Ok(ctx) @@ -242,7 +261,7 @@ pub trait Initializer: Sync + Send { /// Occurs after the app's `after_routes`. /// Use this to compose additional functionality and wire it into an Axum /// Router - async fn after_routes(&self, router: AxumRouter, _ctx: &AppContext) -> Result { + async fn after_routes(&self, router: ApiRouter, _ctx: &AppContext) -> Result { Ok(router) } } diff --git a/src/boot.rs b/src/boot.rs index 1689118de..253ff3d54 100644 --- a/src/boot.rs +++ b/src/boot.rs @@ -3,7 +3,7 @@ //! your application. use std::path::PathBuf; -use axum::Router; +use aide::axum::ApiRouter; #[cfg(feature = "with-db")] use sea_orm_migration::MigratorTrait; use tokio::{select, signal, task::JoinHandle}; @@ -44,7 +44,7 @@ pub struct BootResult { /// Application Context pub app_context: AppContext, /// Web server routes - pub router: Option, + pub router: Option, /// worker processor pub run_worker: bool, } diff --git a/src/controller/app_routes.rs b/src/controller/app_routes.rs index d9d812ade..6186060c5 100644 --- a/src/controller/app_routes.rs +++ b/src/controller/app_routes.rs @@ -4,7 +4,8 @@ use std::{fmt, sync::OnceLock}; -use axum::Router as AXRouter; +use aide::axum::{routing::ApiMethodRouter, ApiRouter}; + use regex::Regex; use crate::{ @@ -26,11 +27,10 @@ pub struct AppRoutes { routes: Vec, } -#[derive(Debug)] pub struct ListRoutes { pub uri: String, pub actions: Vec, - pub method: axum::routing::MethodRouter, + pub method: ApiMethodRouter, } impl fmt::Display for ListRoutes { @@ -170,8 +170,8 @@ impl AppRoutes { pub fn to_router( &self, ctx: AppContext, - mut app: AXRouter, - ) -> Result { + mut app: ApiRouter, + ) -> Result { // IMPORTANT: middleware ordering in this function is opposite to what you // intuitively may think. when using `app.layer` to add individual middleware, // the LAST middleware is the FIRST to meet the outside world (a user request @@ -190,10 +190,10 @@ impl AppRoutes { // issues in compile times itself (https://github.com/rust-lang/crates.io/pull/7443). // for router in self.collect() { + // panic!("Adding {} to router", router.uri); tracing::info!("{}", router.to_string()); - app = app.route(&router.uri, router.method); + app = app.api_route(&router.uri, router.method); } - let middlewares = self.middlewares::(&ctx); for mid in middlewares { app = mid.apply(app)?; diff --git a/src/controller/describe.rs b/src/controller/describe.rs index e3ab19f2c..a6bbbb0c1 100644 --- a/src/controller/describe.rs +++ b/src/controller/describe.rs @@ -1,5 +1,6 @@ use std::sync::OnceLock; +use aide::axum::routing::ApiMethodRouter; use axum::{http, routing::MethodRouter}; use regex::Regex; @@ -16,8 +17,8 @@ fn get_describe_method_action() -> &'static Regex { /// Currently axum not exposed the action type of the router. for hold extra /// information about routers we need to convert the `method` to string and /// capture the details -pub fn method_action(method: &MethodRouter) -> Vec { - let method_str = format!("{method:?}"); +pub fn method_action(method: &ApiMethodRouter) -> Vec { + let method_str = format!("{:?}", MethodRouter::::from(method.clone())); get_describe_method_action() .captures(&method_str) diff --git a/src/controller/format.rs b/src/controller/format.rs index 9db506ef9..e770fc246 100644 --- a/src/controller/format.rs +++ b/src/controller/format.rs @@ -26,6 +26,7 @@ use axum::{ body::Body, http::{response::Builder, HeaderName, HeaderValue}, response::{Html, IntoResponse, Redirect, Response}, + Json, }; use axum_extra::extract::cookie::Cookie; use bytes::{BufMut, BytesMut}; @@ -34,10 +35,7 @@ use serde::Serialize; use serde_json::json; use crate::{ - controller::{ - views::{self, ViewRenderer}, - Json, - }, + controller::views::{self, ViewRenderer}, Result, }; diff --git a/src/controller/health.rs b/src/controller/health.rs index 5dd5ac0f8..b36c1512e 100644 --- a/src/controller/health.rs +++ b/src/controller/health.rs @@ -2,21 +2,23 @@ //! reporting. These routes are commonly used to monitor the health of the //! application and its dependencies. -use axum::{extract::State, response::Response, routing::get}; +use aide::axum::routing::get; +use axum::{extract::State, Json}; +use schemars::JsonSchema; use serde::Serialize; -use super::{format, routes::Routes}; -use crate::{app::AppContext, Result}; +use super::routes::Routes; +use crate::app::AppContext; /// Represents the health status of the application. -#[derive(Serialize)] +#[derive(Serialize, JsonSchema)] struct Health { pub ok: bool, } /// Check the healthiness of the application bt ping to the redis and the DB to /// insure that connection -async fn health(State(ctx): State) -> Result { +async fn health(State(ctx): State) -> Json { let mut is_ok = match ctx.db.ping().await { Ok(()) => true, Err(error) => { @@ -31,7 +33,7 @@ async fn health(State(ctx): State) -> Result { is_ok = false; } } - format::json(Health { ok: is_ok }) + Json(Health { ok: is_ok }) } /// Defines and returns the health-related routes. diff --git a/src/controller/middleware/auth.rs b/src/controller/middleware/auth.rs index aeb648568..8b0b95510 100644 --- a/src/controller/middleware/auth.rs +++ b/src/controller/middleware/auth.rs @@ -21,6 +21,7 @@ //! ``` use std::collections::HashMap; +use aide::OperationInput; use axum::{ extract::{FromRef, FromRequestParts, Query}, http::{request::Parts, HeaderMap}, @@ -51,6 +52,8 @@ pub struct JWTWithUser { pub user: T, } +impl OperationInput for JWTWithUser {} + // Implement the FromRequestParts trait for the Auth struct impl FromRequestParts for JWTWithUser where @@ -89,6 +92,8 @@ pub struct JWT { pub claims: auth::jwt::UserClaims, } +impl OperationInput for JWT {} + // Implement the FromRequestParts trait for the Auth struct impl FromRequestParts for JWT where diff --git a/src/controller/middleware/catch_panic.rs b/src/controller/middleware/catch_panic.rs index 2214e220b..82e514376 100644 --- a/src/controller/middleware/catch_panic.rs +++ b/src/controller/middleware/catch_panic.rs @@ -5,7 +5,7 @@ //! internal server error response. This middleware helps ensure that the //! application can gracefully handle unexpected errors without crashing the //! server. -use axum::Router as AXRouter; +use aide::axum::ApiRouter; use serde::{Deserialize, Serialize}; use tower_http::catch_panic::CatchPanicLayer; @@ -53,7 +53,7 @@ impl MiddlewareLayer for CatchPanic { } /// Applies the Catch Panic middleware layer to the Axum router. - fn apply(&self, app: AXRouter) -> Result> { + fn apply(&self, app: ApiRouter) -> Result> { Ok(app.layer(CatchPanicLayer::custom(handle_panic))) } } diff --git a/src/controller/middleware/compression.rs b/src/controller/middleware/compression.rs index 42a2de54a..227791d55 100644 --- a/src/controller/middleware/compression.rs +++ b/src/controller/middleware/compression.rs @@ -5,7 +5,7 @@ //! times and reducing bandwidth usage. The middleware configuration allows for //! enabling or disabling compression based on the application settings. -use axum::Router as AXRouter; +use aide::axum::ApiRouter; use serde::{Deserialize, Serialize}; use tower_http::compression::CompressionLayer; @@ -33,7 +33,7 @@ impl MiddlewareLayer for Compression { } /// Applies the Compression middleware layer to the Axum router. - fn apply(&self, app: AXRouter) -> Result> { + fn apply(&self, app: ApiRouter) -> Result> { Ok(app.layer(CompressionLayer::new())) } } diff --git a/src/controller/middleware/cors.rs b/src/controller/middleware/cors.rs index c0d8c53d8..b663972d9 100644 --- a/src/controller/middleware/cors.rs +++ b/src/controller/middleware/cors.rs @@ -7,7 +7,7 @@ use std::time::Duration; -use axum::Router as AXRouter; +use aide::axum::ApiRouter; use serde::{Deserialize, Serialize}; use serde_json::json; use tower_http::cors::{self, Any}; @@ -157,7 +157,7 @@ impl MiddlewareLayer for Cors { } /// Applies the CORS middleware layer to the Axum router. - fn apply(&self, app: AXRouter) -> Result> { + fn apply(&self, app: ApiRouter) -> Result> { Ok(app.layer(self.cors()?)) } } diff --git a/src/controller/middleware/etag.rs b/src/controller/middleware/etag.rs index 87029fd81..b878bd754 100644 --- a/src/controller/middleware/etag.rs +++ b/src/controller/middleware/etag.rs @@ -8,9 +8,8 @@ use std::task::{Context, Poll}; -use axum::{ - body::Body, extract::Request, http::StatusCode, response::Response, Router as AXRouter, -}; +use aide::axum::ApiRouter; +use axum::{body::Body, extract::Request, http::StatusCode, response::Response}; use futures_util::future::BoxFuture; use hyper::header::{ETAG, IF_NONE_MATCH}; use serde::{Deserialize, Serialize}; @@ -40,7 +39,7 @@ impl MiddlewareLayer for Etag { } /// Applies the `ETag` middleware to the application router. - fn apply(&self, app: AXRouter) -> Result> { + fn apply(&self, app: ApiRouter) -> Result> { Ok(app.layer(EtagLayer)) } } diff --git a/src/controller/middleware/fallback.rs b/src/controller/middleware/fallback.rs index 4e1f6dad8..cf911d708 100644 --- a/src/controller/middleware/fallback.rs +++ b/src/controller/middleware/fallback.rs @@ -4,7 +4,8 @@ //! not match. It serves a file, a custom not-found message, or a default HTML //! fallback page based on the configuration. -use axum::{http::StatusCode, response::Html, Router as AXRouter}; +use aide::axum::ApiRouter; +use axum::{http::StatusCode, response::Html}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_json::json; use tower_http::services::ServeFile; @@ -85,7 +86,7 @@ impl MiddlewareLayer for Fallback { } /// Applies the fallback middleware to the application router. - fn apply(&self, app: AXRouter) -> Result> { + fn apply(&self, app: ApiRouter) -> Result> { let app = if let Some(path) = &self.file { app.fallback_service(ServeFile::new(path)) } else if let Some(not_found) = &self.not_found { diff --git a/src/controller/middleware/limit_payload.rs b/src/controller/middleware/limit_payload.rs index ebb5168a5..35d4961d9 100644 --- a/src/controller/middleware/limit_payload.rs +++ b/src/controller/middleware/limit_payload.rs @@ -11,7 +11,7 @@ //! request action to enforce the payload limit correctly. Without this, the //! middleware will not function as intended. -use axum::Router as AXRouter; +use aide::axum::ApiRouter; use serde::{Deserialize, Deserializer, Serialize}; use crate::{app::AppContext, controller::middleware::MiddlewareLayer, Result}; @@ -77,7 +77,7 @@ impl MiddlewareLayer for LimitPayload { /// Applies the payload limit middleware to the application router by adding /// a `DefaultBodyLimit` layer. - fn apply(&self, app: AXRouter) -> Result> { + fn apply(&self, app: ApiRouter) -> Result> { let body_limit_layer = match self.body_limit { DefaultBodyLimitKind::Disable => axum::extract::DefaultBodyLimit::disable(), DefaultBodyLimitKind::Limit(limit) => axum::extract::DefaultBodyLimit::max(limit), diff --git a/src/controller/middleware/logger.rs b/src/controller/middleware/logger.rs index 9041f327b..1667387bc 100644 --- a/src/controller/middleware/logger.rs +++ b/src/controller/middleware/logger.rs @@ -7,7 +7,8 @@ //! into the log context, allowing environment-specific logging (e.g., //! "development", "production"). -use axum::{http, Router as AXRouter}; +use aide::axum::ApiRouter; +use axum::http; use serde::{Deserialize, Serialize}; use tower_http::{add_extension::AddExtensionLayer, trace::TraceLayer}; @@ -66,7 +67,7 @@ impl MiddlewareLayer for Middleware { /// The `TraceLayer` is customized with `make_span_with` to extract /// request-specific details like method, URI, version, user agent, and /// request ID, then create a tracing span for the request. - fn apply(&self, app: AXRouter) -> Result> { + fn apply(&self, app: ApiRouter) -> Result> { Ok(app .layer( TraceLayer::new_for_http().make_span_with(|request: &http::Request<_>| { diff --git a/src/controller/middleware/mod.rs b/src/controller/middleware/mod.rs index 2f55ab455..c8a317b90 100644 --- a/src/controller/middleware/mod.rs +++ b/src/controller/middleware/mod.rs @@ -23,7 +23,7 @@ pub mod secure_headers; pub mod static_assets; pub mod timeout; -use axum::Router as AXRouter; +use aide::axum::ApiRouter; use serde::{Deserialize, Serialize}; use crate::{app::AppContext, environment::Environment, Result}; @@ -64,7 +64,7 @@ pub trait MiddlewareLayer { /// # Errors /// /// If there is an issue when adding the middleware to the router. - fn apply(&self, app: AXRouter) -> Result>; + fn apply(&self, app: ApiRouter) -> Result>; } #[allow(clippy::unnecessary_lazy_evaluations)] diff --git a/src/controller/middleware/powered_by.rs b/src/controller/middleware/powered_by.rs index 5cb2ce01f..93299ec53 100644 --- a/src/controller/middleware/powered_by.rs +++ b/src/controller/middleware/powered_by.rs @@ -8,10 +8,8 @@ use std::sync::OnceLock; -use axum::{ - http::header::{HeaderName, HeaderValue}, - Router as AXRouter, -}; +use aide::axum::ApiRouter; +use axum::http::header::{HeaderName, HeaderValue}; use tower_http::set_header::SetResponseHeaderLayer; use crate::{app::AppContext, controller::middleware::MiddlewareLayer, Result}; @@ -77,7 +75,7 @@ impl MiddlewareLayer for Middleware { /// Applies the middleware to the application by adding the `X-Powered-By` /// header to each response. - fn apply(&self, app: AXRouter) -> Result> { + fn apply(&self, app: ApiRouter) -> Result> { Ok(app.layer(SetResponseHeaderLayer::overriding( HeaderName::from_static("x-powered-by"), self.ident diff --git a/src/controller/middleware/remote_ip.rs b/src/controller/middleware/remote_ip.rs index d862bf050..61e755118 100644 --- a/src/controller/middleware/remote_ip.rs +++ b/src/controller/middleware/remote_ip.rs @@ -17,12 +17,12 @@ use std::{ task::{Context, Poll}, }; +use aide::axum::ApiRouter; use axum::{ body::Body, extract::{ConnectInfo, FromRequestParts, Request}, http::request::Parts, response::Response, - Router as AXRouter, }; use futures_util::future::BoxFuture; use hyper::HeaderMap; @@ -120,7 +120,7 @@ impl MiddlewareLayer for RemoteIpMiddleware { } /// Applies the Remote IP middleware to the given Axum router. - fn apply(&self, app: AXRouter) -> Result> { + fn apply(&self, app: ApiRouter) -> Result> { Ok(app.layer(RemoteIPLayer::new(self)?)) } } diff --git a/src/controller/middleware/request_id.rs b/src/controller/middleware/request_id.rs index d0f1740f5..a4f6f81f5 100644 --- a/src/controller/middleware/request_id.rs +++ b/src/controller/middleware/request_id.rs @@ -6,9 +6,8 @@ //! This can be useful for tracking requests across services, logging, and //! debugging. -use axum::{ - extract::Request, http::HeaderValue, middleware::Next, response::Response, Router as AXRouter, -}; +use aide::axum::ApiRouter; +use axum::{extract::Request, http::HeaderValue, middleware::Next, response::Response}; use regex::Regex; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -54,7 +53,7 @@ impl MiddlewareLayer for RequestId { /// /// # Errors /// This function returns an error if the middleware cannot be applied. - fn apply(&self, app: AXRouter) -> Result> { + fn apply(&self, app: ApiRouter) -> Result> { Ok(app.layer(axum::middleware::from_fn(request_id_middleware))) } } diff --git a/src/controller/middleware/secure_headers.rs b/src/controller/middleware/secure_headers.rs index ff01eaebe..d46aed5f7 100644 --- a/src/controller/middleware/secure_headers.rs +++ b/src/controller/middleware/secure_headers.rs @@ -9,11 +9,11 @@ use std::{ task::{Context, Poll}, }; +use aide::axum::ApiRouter; use axum::{ body::Body, http::{HeaderName, HeaderValue, Request}, response::Response, - Router as AXRouter, }; use futures_util::future::BoxFuture; use serde::{Deserialize, Serialize}; @@ -111,7 +111,7 @@ impl MiddlewareLayer for SecureHeader { } /// Applies the secure headers layer to the application router - fn apply(&self, app: AXRouter) -> Result> { + fn apply(&self, app: ApiRouter) -> Result> { Ok(app.layer(SecureHeaders::new(self)?)) } } diff --git a/src/controller/middleware/static_assets.rs b/src/controller/middleware/static_assets.rs index 0b336b936..0d950a742 100644 --- a/src/controller/middleware/static_assets.rs +++ b/src/controller/middleware/static_assets.rs @@ -11,7 +11,7 @@ use std::path::PathBuf; -use axum::Router as AXRouter; +use aide::axum::ApiRouter; use serde::{Deserialize, Serialize}; use serde_json::json; use tower_http::services::{ServeDir, ServeFile}; @@ -88,14 +88,14 @@ impl MiddlewareLayer for StaticAssets { /// Applies the static assets middleware to the application router. /// - /// This method wraps the provided [`AXRouter`] with a service to serve + /// This method wraps the provided [`ApiRouter`] with a service to serve /// static files from the folder specified in the configuration. It will /// serve a fallback file if the requested file is not found, and can /// also serve precompressed (gzip) files if enabled. /// /// Before applying, it checks if the folder and fallback file exist. If /// either is missing, it returns an error. - fn apply(&self, app: AXRouter) -> Result> { + fn apply(&self, app: ApiRouter) -> Result> { if self.must_exist && (!PathBuf::from(&self.folder.path).exists() || !PathBuf::from(&self.fallback).exists()) diff --git a/src/controller/middleware/timeout.rs b/src/controller/middleware/timeout.rs index fc0a5c16e..9a7cb0563 100644 --- a/src/controller/middleware/timeout.rs +++ b/src/controller/middleware/timeout.rs @@ -11,7 +11,7 @@ //! the request took too long to process. use std::time::Duration; -use axum::Router as AXRouter; +use aide::axum::ApiRouter; use serde::{Deserialize, Serialize}; use serde_json::json; use tower_http::timeout::TimeoutLayer; @@ -55,10 +55,10 @@ impl MiddlewareLayer for TimeOut { /// Applies the timeout middleware to the application router. /// - /// This method wraps the provided [`AXRouter`] in a [`TimeoutLayer`], + /// This method wraps the provided [`ApiRouter`] in a [`TimeoutLayer`], /// ensuring that requests exceeding the specified timeout duration will /// be interrupted. - fn apply(&self, app: AXRouter) -> Result> { + fn apply(&self, app: ApiRouter) -> Result> { Ok(app.layer(TimeoutLayer::new(Duration::from_millis(self.timeout)))) } } diff --git a/src/controller/mod.rs b/src/controller/mod.rs index 51cba3f69..1801e1786 100644 --- a/src/controller/mod.rs +++ b/src/controller/mod.rs @@ -67,9 +67,9 @@ pub use app_routes::{AppRoutes, ListRoutes}; use axum::{ - extract::FromRequest, http::StatusCode, response::{IntoResponse, Response}, + Json, }; use colored::Colorize; pub use routes::Routes; @@ -166,16 +166,6 @@ impl ErrorDetail { } } -#[derive(Debug, FromRequest)] -#[from_request(via(axum::Json), rejection(Error))] -pub struct Json(pub T); - -impl IntoResponse for Json { - fn into_response(self) -> axum::response::Response { - axum::Json(self.0).into_response() - } -} - impl IntoResponse for Error { /// Convert an `Error` into an HTTP response. #[allow(clippy::cognitive_complexity)] diff --git a/src/controller/ping.rs b/src/controller/ping.rs index 078e32cb0..cff1aefb0 100644 --- a/src/controller/ping.rs +++ b/src/controller/ping.rs @@ -2,21 +2,22 @@ //! reporting. These routes are commonly used to monitor the health of the //! application and its dependencies. -use axum::{response::Response, routing::get}; +use aide::axum::{routing::get, IntoApiResponse}; +use axum::Json; +use schemars::JsonSchema; use serde::Serialize; -use super::{format, routes::Routes}; -use crate::Result; +use super::routes::Routes; /// Represents the health status of the application. -#[derive(Serialize)] +#[derive(Serialize, JsonSchema)] struct Health { pub ok: bool, } /// Check application ping endpoint -async fn ping() -> Result { - format::json(Health { ok: true }) +async fn ping() -> impl IntoApiResponse { + Json(Health { ok: true }) } /// Defines and returns the health-related routes. diff --git a/src/controller/routes.rs b/src/controller/routes.rs index c4f9c6b8f..18308b5bd 100644 --- a/src/controller/routes.rs +++ b/src/controller/routes.rs @@ -1,6 +1,11 @@ use std::convert::Infallible; -use axum::{extract::Request, response::IntoResponse, routing::Route}; +use aide::axum::routing::ApiMethodRouter; +use axum::{ + extract::Request, + response::IntoResponse, + routing::{MethodRouter, Route}, +}; use tower::{Layer, Service}; use super::describe; @@ -12,13 +17,26 @@ pub struct Routes { // pub version: Option, } -#[derive(Clone, Default, Debug)] +#[derive(Clone, Default)] pub struct Handler { pub uri: String, - pub method: axum::routing::MethodRouter, + pub method: ApiMethodRouter, pub actions: Vec, } +impl std::fmt::Debug for Handler { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Handler") + .field("uri", &self.uri) + .field( + "method", + &Into::>::into(self.method.clone()), + ) + .field("actions", &self.actions) + .finish() + } +} + impl Routes { /// Creates a new [`Routes`] instance with default settings. #[must_use] @@ -78,7 +96,7 @@ impl Routes { /// Routes::new().add("/_ping", get(ping)); /// ```` #[must_use] - pub fn add(mut self, uri: &str, method: axum::routing::MethodRouter) -> Self { + pub fn add(mut self, uri: &str, method: ApiMethodRouter) -> Self { describe::method_action(&method); self.handlers.push(Handler { uri: uri.to_owned(), diff --git a/src/initializers/extra_db.rs b/src/initializers/extra_db.rs index 174a57677..e8a725312 100644 --- a/src/initializers/extra_db.rs +++ b/src/initializers/extra_db.rs @@ -1,5 +1,6 @@ +use aide::axum::ApiRouter; use async_trait::async_trait; -use axum::{Extension, Router as AxumRouter}; +use axum::Extension; use crate::{ app::{AppContext, Initializer}, @@ -15,7 +16,7 @@ impl Initializer for ExtraDbInitializer { "extra_db".to_string() } - async fn after_routes(&self, router: AxumRouter, ctx: &AppContext) -> Result { + async fn after_routes(&self, router: ApiRouter, ctx: &AppContext) -> Result { let extra_db_config = ctx .config .initializers diff --git a/src/initializers/multi_db.rs b/src/initializers/multi_db.rs index 5f9a91846..fea3909e7 100644 --- a/src/initializers/multi_db.rs +++ b/src/initializers/multi_db.rs @@ -1,5 +1,6 @@ +use aide::axum::ApiRouter; use async_trait::async_trait; -use axum::{Extension, Router as AxumRouter}; +use axum::Extension; use crate::{ app::{AppContext, Initializer}, @@ -15,7 +16,7 @@ impl Initializer for MultiDbInitializer { "multi_db".to_string() } - async fn after_routes(&self, router: AxumRouter, ctx: &AppContext) -> Result { + async fn after_routes(&self, router: ApiRouter, ctx: &AppContext) -> Result { let settings = ctx .config .initializers diff --git a/src/prelude.rs b/src/prelude.rs index 46c341919..006f3b169 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -1,8 +1,10 @@ +pub use aide::axum::routing::{delete, get, head, options, patch, post, put, trace}; pub use async_trait::async_trait; + pub use axum::{ extract::{Form, Path, State}, response::{IntoResponse, Response}, - routing::{delete, get, head, options, patch, post, put, trace}, + Json, }; pub use axum_extra::extract::cookie; pub use chrono::NaiveDateTime as DateTime; @@ -35,7 +37,7 @@ pub use crate::{ }, not_found, unauthorized, views::{engines::TeraView, ViewEngine, ViewRenderer}, - Json, Routes, + Routes, }, errors::Error, mailer, diff --git a/src/testing/request.rs b/src/testing/request.rs index 1ed0b578f..196a0dfdd 100644 --- a/src/testing/request.rs +++ b/src/testing/request.rs @@ -1,5 +1,6 @@ use std::net::SocketAddr; +use aide::openapi::OpenApi; use axum_test::{TestServer, TestServerConfig}; use crate::{ @@ -81,9 +82,11 @@ where default_content_type: Some("application/json".to_string()), ..Default::default() }; + let mut api = OpenApi::default(); let server = TestServer::new_with_config( boot.router .unwrap() + .finish_api(&mut api) .into_make_service_with_connect_info::(), config, )