Skip to content

Commit e0b12c1

Browse files
feat(axum): typed path support (#214)
1 parent b0563bb commit e0b12c1

File tree

6 files changed

+214
-1
lines changed

6 files changed

+214
-1
lines changed

crates/aide-macros/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,7 @@ proc-macro = true
1313
darling = "0.20.0"
1414
quote = "1.0.21"
1515
syn = "2.0.15"
16+
proc-macro2 = "1.0"
17+
18+
[features]
19+
axum-extra-typed-routing = []

crates/aide-macros/src/lib.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,3 +166,26 @@ pub fn derive_operation_io(ts: TokenStream) -> TokenStream {
166166

167167
ts.into()
168168
}
169+
170+
#[cfg(feature = "axum-extra-typed-routing")]
171+
/// Example usage:
172+
/// ```ignore
173+
/// #[aide::axum_typed_path]
174+
/// #[typed_path("/foo/bar")]
175+
/// struct FooBar;
176+
/// ```
177+
#[proc_macro_attribute] // functions tagged with `#[proc_macro_attribute]` must currently reside in the root of the crate
178+
pub fn axum_typed_path(_attr: TokenStream, item: TokenStream) -> TokenStream {
179+
let input = proc_macro2::TokenStream::from(item);
180+
quote! {
181+
#[derive(
182+
::axum_extra::routing::TypedPath,
183+
::aide_macros::OperationIo,
184+
::schemars::JsonSchema,
185+
::serde::Deserialize,
186+
)]
187+
#[aide(input_with = "aide::axum::routing::typed::TypedPath<Self>")]
188+
#input
189+
}
190+
.into()
191+
}

crates/aide/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Changelog
22
## Latest
3+
- **added:** Basic `axum` `TypedPath` support and `TypedPath<T>` description from `T` doc comment([#212])
34
- **added:** Add methods for attaching documentation to existing routes ([#203])
45
- **fixed:** Fix `route_with_tsr` incorrect handling ([#203])
56

crates/aide/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ http = { version = "1.0.0", optional = true }
2323
serde_qs = { version = "0.14", optional = true }
2424

2525
axum = { version = "0.8.1", optional = true, default-features = false }
26-
axum-extra = { version = "0.10.0", optional = true }
26+
axum-extra = { version = "0.10", optional = true }
2727
tower-layer = { version = "0.3", optional = true }
2828
tower-service = { version = "0.3", optional = true }
2929
cfg-if = "1.0.0"
@@ -53,6 +53,7 @@ axum-extra-form = ["axum-extra", "axum-extra/form"]
5353
axum-extra-headers = ["axum-extra/typed-header"]
5454
axum-extra-query = ["axum-extra", "axum-extra/query"]
5555
axum-extra-json-deserializer = ["axum-extra", "axum-extra/json-deserializer"]
56+
axum-extra-typed-routing = ["axum-extra", "axum-extra/typed-routing"]
5657

5758
[dev-dependencies]
5859
tokio = { version = "1.21.0", features = ["macros", "rt-multi-thread"] }
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
//! Method routing that closely mimics [`axum::routing`] while extending
22
//! it with API documentation-specific features..
3+
#[cfg(feature = "axum-extra-typed-routing")]
4+
pub mod typed;
35

46
use std::{convert::Infallible, mem};
57

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
//! Typed routing for Axum.
2+
3+
use axum::extract::rejection::PathRejection;
4+
use axum_extra::routing::SecondElementIs;
5+
use http::request::Parts;
6+
use schemars::JsonSchema;
7+
use serde::de::DeserializeOwned;
8+
9+
use super::*;
10+
use crate::operation::{add_parameters, parameters_from_schema, OperationHandler, ParamLocation};
11+
12+
impl<S> crate::axum::ApiRouter<S>
13+
where
14+
S: Clone + Send + Sync + 'static,
15+
{
16+
/// Add a typed `GET` route to the router.
17+
///
18+
/// The path will be inferred from the first argument to the handler function which must
19+
/// implement [`TypedPath`].
20+
pub fn typed_get<H, I, O, T, P>(self, handler: H) -> Self
21+
where
22+
H: axum::handler::Handler<T, S> + OperationHandler<I, O>,
23+
T: SecondElementIs<P> + 'static,
24+
I: OperationInput,
25+
O: OperationOutput,
26+
P: axum_extra::routing::TypedPath + schemars::JsonSchema + OperationInput,
27+
{
28+
self.api_route(P::PATH, crate::axum::routing::get(handler))
29+
}
30+
31+
/// Add a typed `DELETE` route to the router.
32+
///
33+
/// The path will be inferred from the first argument to the handler function which must
34+
/// implement [`TypedPath`].
35+
pub fn typed_delete<H, I, O, T, P>(self, handler: H) -> Self
36+
where
37+
H: axum::handler::Handler<T, S> + OperationHandler<I, O>,
38+
T: SecondElementIs<P> + 'static,
39+
I: OperationInput,
40+
O: OperationOutput,
41+
P: axum_extra::routing::TypedPath + schemars::JsonSchema + OperationInput,
42+
{
43+
self.api_route(P::PATH, crate::axum::routing::delete(handler))
44+
}
45+
46+
/// Add a typed `HEAD` route to the router.
47+
///
48+
/// The path will be inferred from the first argument to the handler function which must
49+
/// implement [`TypedPath`].
50+
pub fn typed_head<H, I, O, T, P>(self, handler: H) -> Self
51+
where
52+
H: axum::handler::Handler<T, S> + OperationHandler<I, O>,
53+
T: SecondElementIs<P> + 'static,
54+
I: OperationInput,
55+
O: OperationOutput,
56+
P: axum_extra::routing::TypedPath + schemars::JsonSchema + OperationInput,
57+
{
58+
self.api_route(P::PATH, crate::axum::routing::head(handler))
59+
}
60+
61+
/// Add a typed `OPTIONS` route to the router.
62+
///
63+
/// The path will be inferred from the first argument to the handler function which must
64+
/// implement [`TypedPath`].
65+
pub fn typed_options<H, I, O, T, P>(self, handler: H) -> Self
66+
where
67+
H: axum::handler::Handler<T, S> + OperationHandler<I, O>,
68+
T: SecondElementIs<P> + 'static,
69+
I: OperationInput,
70+
O: OperationOutput,
71+
P: axum_extra::routing::TypedPath + schemars::JsonSchema + OperationInput,
72+
{
73+
self.api_route(P::PATH, crate::axum::routing::options(handler))
74+
}
75+
76+
/// Add a typed `PATCH` route to the router.
77+
///
78+
/// The path will be inferred from the first argument to the handler function which must
79+
/// implement [`TypedPath`].
80+
pub fn typed_patch<H, I, O, T, P>(self, handler: H) -> Self
81+
where
82+
H: axum::handler::Handler<T, S> + OperationHandler<I, O>,
83+
T: SecondElementIs<P> + 'static,
84+
I: OperationInput,
85+
O: OperationOutput,
86+
P: axum_extra::routing::TypedPath + schemars::JsonSchema + OperationInput,
87+
{
88+
self.api_route(P::PATH, crate::axum::routing::patch(handler))
89+
}
90+
91+
/// Add a typed `POST` route to the router.
92+
///
93+
/// The path will be inferred from the first argument to the handler function which must
94+
/// implement [`TypedPath`].
95+
pub fn typed_post<H, I, O, T, P>(self, handler: H) -> Self
96+
where
97+
H: axum::handler::Handler<T, S> + OperationHandler<I, O>,
98+
T: SecondElementIs<P> + 'static,
99+
I: OperationInput,
100+
O: OperationOutput,
101+
P: axum_extra::routing::TypedPath + schemars::JsonSchema + OperationInput,
102+
{
103+
self.api_route(P::PATH, crate::axum::routing::post(handler))
104+
}
105+
106+
/// Add a typed `PUT` route to the router.
107+
///
108+
/// The path will be inferred from the first argument to the handler function which must
109+
/// implement [`TypedPath`].
110+
pub fn typed_put<H, I, O, T, P>(self, handler: H) -> Self
111+
where
112+
H: axum::handler::Handler<T, S> + OperationHandler<I, O>,
113+
T: SecondElementIs<P> + 'static,
114+
I: OperationInput,
115+
O: OperationOutput,
116+
P: axum_extra::routing::TypedPath + schemars::JsonSchema + OperationInput,
117+
{
118+
self.api_route(P::PATH, crate::axum::routing::put(handler))
119+
}
120+
121+
/// Add a typed `TRACE` route to the router.
122+
///
123+
/// The path will be inferred from the first argument to the handler function which must
124+
/// implement [`TypedPath`].
125+
pub fn typed_trace<H, I, O, T, P>(self, handler: H) -> Self
126+
where
127+
H: axum::handler::Handler<T, S> + OperationHandler<I, O>,
128+
T: SecondElementIs<P> + 'static,
129+
I: OperationInput,
130+
O: OperationOutput,
131+
P: axum_extra::routing::TypedPath + schemars::JsonSchema + OperationInput,
132+
{
133+
self.api_route(P::PATH, crate::axum::routing::trace(handler))
134+
}
135+
}
136+
137+
/// A wrapper around `axum_extra::routing::TypedPath` to implement `OperationInput`.
138+
/// Basically fix for Rust does not support `!Trait` and specialization on stable.
139+
#[derive(Debug)]
140+
pub struct TypedPath<T: axum_extra::routing::TypedPath + JsonSchema>(pub T);
141+
142+
impl<T> OperationInput for TypedPath<T>
143+
where
144+
T: axum_extra::routing::TypedPath + JsonSchema,
145+
{
146+
fn operation_input(ctx: &mut crate::generate::GenContext, operation: &mut Operation) {
147+
// `subschema_for` `description` is none, while `root_schema_for` is some
148+
let schema = ctx.schema.root_schema_for::<T>().schema;
149+
operation.description = schema.metadata.as_ref().and_then(|x| x.description.clone());
150+
let params = parameters_from_schema(ctx, schema, ParamLocation::Path);
151+
add_parameters(ctx, operation, params);
152+
}
153+
}
154+
155+
impl<T, S> axum::extract::FromRequestParts<S> for TypedPath<T>
156+
where
157+
T: DeserializeOwned + Send + axum_extra::routing::TypedPath + JsonSchema,
158+
S: Send + Sync,
159+
{
160+
type Rejection = PathRejection;
161+
162+
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
163+
let path = axum::extract::Path::<T>::from_request_parts(parts, state).await?;
164+
Ok(Self(path.0))
165+
}
166+
}
167+
168+
impl<T, S> axum::extract::OptionalFromRequestParts<S> for TypedPath<T>
169+
where
170+
T: DeserializeOwned + Send + 'static + axum_extra::routing::TypedPath + JsonSchema,
171+
S: Send + Sync,
172+
{
173+
type Rejection = PathRejection;
174+
175+
async fn from_request_parts(
176+
parts: &mut Parts,
177+
state: &S,
178+
) -> Result<Option<Self>, Self::Rejection> {
179+
let path = axum::extract::Path::<T>::from_request_parts(parts, state).await?;
180+
Ok(path.map(|x| Self(x.0)))
181+
}
182+
}

0 commit comments

Comments
 (0)