Skip to content

Commit

Permalink
Add utoipa-axum binding example and update docs (#1007)
Browse files Browse the repository at this point in the history
This PR adds new `axum` app example to demonstrate new seamless
integration between axum and utoipa.

Along the above this commit streamlines the user facing API in
`utoipa-axum`, fixes some minor bugs, update docs and adds one extra
feature which allows using expressions e.g. `const` references in
`tags((name = ...))` attribute. This allows sharing a single `const`
reference between OpenApi and the handlers annotated with 
`#[utoipa::path(...)]`

Fixes #780
  • Loading branch information
juhaku committed Aug 29, 2024
1 parent bcc4fca commit 908d279
Show file tree
Hide file tree
Showing 12 changed files with 274 additions and 55 deletions.
16 changes: 16 additions & 0 deletions examples/axum-utoipa-bindings/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "axum-utoipa-bindings"
version = "0.1.0"
edition = "2021"

[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
tower = "0.5"
utoipa = { path = "../../utoipa", features = ["axum_extras", "debug"] }
utoipa-swagger-ui = { path = "../../utoipa-swagger-ui", features = ["axum"] }
utoipa-axum = { path = "../../utoipa-axum" ,features = ["debug"] }
serde = "1"
serde_json = "1"

[workspace]
14 changes: 14 additions & 0 deletions examples/axum-utoipa-bindings/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# utoipa with axum bindings

This demo `axum` application demonstrates `utoipa` and `axum` seamless integration with `utoipa-axum` crate.
API doc is served via Swagger UI.

Run the app
```bash
cargo run
```

Browse the API docs.
```
http://localhost:8080/swagger-ui
```
128 changes: 128 additions & 0 deletions examples/axum-utoipa-bindings/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
use std::io;
use std::net::Ipv4Addr;

use tokio::net::TcpListener;
use utoipa::OpenApi;
use utoipa_axum::router::OpenApiRouter;
use utoipa_axum::routes;
use utoipa_swagger_ui::SwaggerUi;

const CUSTOMER_TAG: &str = "customer";
const ORDER_TAG: &str = "order";

#[derive(OpenApi)]
#[openapi(
tags(
(name = CUSTOMER_TAG, description = "Customer API endpoints"),
(name = ORDER_TAG, description = "Order API endpoints")
)
)]
struct ApiDoc;

/// Get health of the API.
#[utoipa::path(
method(get, head),
path = "/api/health",
responses(
(status = OK, description = "Success", body = str, content_type = "text/plain")
)
)]
async fn health() -> &'static str {
"ok"
}

#[tokio::main]
async fn main() -> Result<(), io::Error> {
let (router, api) = OpenApiRouter::with_openapi(ApiDoc::openapi())
.routes(routes!(health))
.nest("/api/customer", customer::router())
.nest("/api/order", order::router())
.split_for_parts();

let router = router.merge(SwaggerUi::new("/swagger-ui").url("/apidoc/openapi.json", api));

let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 8080)).await?;
axum::serve(listener, router).await
}

mod customer {
use axum::Json;
use serde::Serialize;
use utoipa::{OpenApi, ToSchema};
use utoipa_axum::router::OpenApiRouter;
use utoipa_axum::routes;

#[derive(OpenApi)]
#[openapi(components(schemas(Customer)))]
struct CustomerApi;

/// This is the customer
#[derive(ToSchema, Serialize)]
struct Customer {
name: String,
}

/// expose the Customer OpenAPI to parent module
pub fn router() -> OpenApiRouter {
OpenApiRouter::with_openapi(CustomerApi::openapi()).routes(routes!(get_customer))
}

/// Get customer
///
/// Just return a static Customer object
#[utoipa::path(get, path = "", responses((status = OK, body = Customer)), tag = super::CUSTOMER_TAG)]
async fn get_customer() -> Json<Customer> {
Json(Customer {
name: String::from("Bill Book"),
})
}
}

mod order {
use axum::Json;
use serde::{Deserialize, Serialize};
use utoipa::{OpenApi, ToSchema};
use utoipa_axum::router::OpenApiRouter;
use utoipa_axum::routes;

#[derive(OpenApi)]
#[openapi(components(schemas(Order, OrderRequest)))]
struct OrderApi;

/// This is the order
#[derive(ToSchema, Serialize)]
struct Order {
id: i32,
name: String,
}

#[derive(ToSchema, Deserialize, Serialize)]
struct OrderRequest {
name: String,
}

/// expose the Order OpenAPI to parent module
pub fn router() -> OpenApiRouter {
OpenApiRouter::with_openapi(OrderApi::openapi()).routes(routes!(get_order, create_order))
}

/// Get static order object
#[utoipa::path(get, path = "", responses((status = OK, body = Order)), tag = super::ORDER_TAG)]
async fn get_order() -> Json<Order> {
Json(Order {
id: 100,
name: String::from("Bill Book"),
})
}

/// Create an order.
///
/// Create an order by basically passing through the name of the request with static id.
#[utoipa::path(post, path = "", responses((status = OK, body = OrderRequest)), tag = super::ORDER_TAG)]
async fn create_order(Json(order): Json<OrderRequest>) -> Json<Order> {
Json(Order {
id: 120,
name: order.name,
})
}
}
12 changes: 6 additions & 6 deletions utoipa-axum/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ Utoipa axum brings `utoipa` and `axum` closer together by the way of providing a
the `axum` API. It gives a natural way to register handlers known to `axum` and also simultaneously generates OpenAPI
specification from the handlers.

## Crate features

- **`debug`**: Implement debug traits for types.

## Install

Add dependency declaration to `Cargo.toml`.
Expand All @@ -28,12 +32,8 @@ struct Todo {
struct Api;

let mut router: OpenApiRouter = OpenApiRouter::with_openapi(Api::openapi())
.routes(get_path(search_user))
.routes(
get_path(get_user)
.post_path(post_user)
.delete_path(delete_user),
);
.routes(routes!(search_user))
.routes(routes!(get_user, post_user, delete_user));

let api = router.to_openapi();
let axum_router: axum::Router = router.into();
Expand Down
4 changes: 4 additions & 0 deletions utoipa-axum/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
//! the `axum` API. It gives a natural way to register handlers known to `axum` and also simultaneously generates OpenAPI
//! specification from the handlers.
//!
//! ## Crate features
//!
//! - **`debug`**: Implement debug traits for types.
//!
//! ## Install
//!
//! Add dependency declaration to `Cargo.toml`.
Expand Down
50 changes: 22 additions & 28 deletions utoipa-axum/src/router.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
//! Implements Router for composing handlers and collecting OpenAPI information.
use std::collections::BTreeMap;
use std::convert::Infallible;

use axum::extract::Request;
Expand Down Expand Up @@ -32,7 +31,7 @@ pub type UtoipaMethodRouter<S = ()> =
/// A wrapper struct for [`axum::Router`] and [`utoipa::openapi::OpenApi`] for composing handlers
/// and services with collecting OpenAPI information from the handlers.
///
/// This struct provides passthrough implementation for most of the [`axum::Router`] methods and
/// This struct provides pass through implementation for most of the [`axum::Router`] methods and
/// extends capabilities for few to collect the OpenAPI information. Methods that are not
/// implemented can be easily called after converting this router to [`axum::Router`] by
/// [`Into::into`].
Expand Down Expand Up @@ -76,12 +75,12 @@ where
Self(Router::new(), openapi)
}

/// Passthrough method for [`axum::Router::as_service`].
/// Pass through method for [`axum::Router::as_service`].
pub fn as_service<B>(&mut self) -> RouterAsService<'_, B, S> {
self.0.as_service()
}

/// Passthrough method for [`axum::Router::fallback`].
/// Pass through method for [`axum::Router::fallback`].
pub fn fallback<H, T>(self, handler: H) -> Self
where
H: Handler<T, S>,
Expand All @@ -90,7 +89,7 @@ where
Self(self.0.fallback(handler), self.1)
}

/// Passthrough method for [`axum::Router::fallback_service`].
/// Pass through method for [`axum::Router::fallback_service`].
pub fn fallback_service<T>(self, service: T) -> Self
where
T: Service<Request, Error = Infallible> + Clone + Send + 'static,
Expand All @@ -100,7 +99,7 @@ where
Self(self.0.fallback_service(service), self.1)
}

/// Passthrough method for [`axum::Router::layer`].
/// Pass through method for [`axum::Router::layer`].
pub fn layer<L>(self, layer: L) -> Self
where
L: Layer<Route> + Clone + Send + 'static,
Expand All @@ -115,7 +114,7 @@ where
/// Register [`UtoipaMethodRouter`] content created with [`routes`][routes] macro to `self`.
///
/// Paths of the [`UtoipaMethodRouter`] will be extended to [`utoipa::openapi::OpenApi`] and
/// [`axum::router::MethodRouter`] will be added to the [`axum::Router`].
/// [`axum::routing::MethodRouter`] will be added to the [`axum::Router`].
///
/// [routes]: ../macro.routes.html
pub fn routes(mut self, (mut paths, method_router): UtoipaMethodRouter<S>) -> Self {
Expand All @@ -141,12 +140,12 @@ where
Self(router, self.1)
}

/// Passthrough method for [`axum::Router<S>::route`].
/// Pass through method for [`axum::Router<S>::route`].
pub fn route(self, path: &str, method_router: MethodRouter<S>) -> Self {
Self(self.0.route(&colonized_params(path), method_router), self.1)
}

/// Passthrough method for [`axum::Router::route_layer`].
/// Pass through method for [`axum::Router::route_layer`].
pub fn route_layer<L>(self, layer: L) -> Self
where
L: Layer<Route> + Clone + Send + 'static,
Expand All @@ -158,7 +157,7 @@ where
Self(self.0.route_layer(layer), self.1)
}

/// Passthrough method for [`axum::Router<S>::route_service`].
/// Pass through method for [`axum::Router<S>::route_service`].
pub fn route_service<T>(self, path: &str, service: T) -> Self
where
T: Service<Request, Error = Infallible> + Clone + Send + 'static,
Expand Down Expand Up @@ -189,26 +188,15 @@ where
/// let router: OpenApiRouter = OpenApiRouter::new()
/// .nest("/api", search_router);
/// ```
pub fn nest(mut self, path: &str, router: OpenApiRouter<S>) -> Self {
let nested_paths = router
.1
.paths
.paths
.into_iter()
.map(|(item_path, item)| {
let path = format!("{path}{item_path}");
(path, item)
})
.collect::<BTreeMap<_, _>>();

self.1.paths.paths.extend(nested_paths);

pub fn nest(self, path: &str, router: OpenApiRouter<S>) -> Self {
let api = self.1.nest(path, router.1);
let path = if path.is_empty() { "/" } else { path };
let router = self.0.nest(&colonized_params(path), router.0);
Self(router, self.1)

Self(router, api)
}

/// Passthrough method for [`axum::Router::nest_service`]. _**This does nothing for OpenApi paths.**_
/// Pass through method for [`axum::Router::nest_service`]. _**This does nothing for OpenApi paths.**_
pub fn nest_service<T>(self, path: &str, service: T) -> Self
where
T: Service<Request, Error = Infallible> + Clone + Send + 'static,
Expand Down Expand Up @@ -240,12 +228,12 @@ where
/// .merge(search_router);
/// ```
pub fn merge(mut self, router: OpenApiRouter<S>) -> Self {
self.1.paths.paths.extend(router.1.paths.paths);
self.1.merge(router.1);

Self(self.0.merge(router.0), self.1)
}

/// Passthrough method for [`axum::Router::with_state`].
/// Pass through method for [`axum::Router::with_state`].
pub fn with_state<S2>(self, state: S) -> OpenApiRouter<S2> {
OpenApiRouter(self.0.with_state(state), self.1)
}
Expand All @@ -265,6 +253,12 @@ where
pub fn get_openapi(&self) -> &utoipa::openapi::OpenApi {
&self.1
}

/// Split the content of the [`OpenApiRouter`] to parts. Method will return a tuple of
/// inner [`axum::Router`] and [`utoipa::openapi::OpenApi`].
pub fn split_for_parts(self) -> (axum::Router<S>, utoipa::openapi::OpenApi) {
(self.0, self.1)
}
}

impl<S> Default for OpenApiRouter<S>
Expand Down
Loading

0 comments on commit 908d279

Please sign in to comment.