Skip to content

Commit

Permalink
Add support for tuple Path parameters for axum (#388)
Browse files Browse the repository at this point in the history
Add support for axum to use tuple style `Path` parameters. This PR add
suport for syntax below:
```rust
    get,
    path = "/person/{id}/{name}",
    params(
        ("id", description = "Person id"),
        ("name", description = "Person name")
    ),
    responses(
        (status = 200, description = "success response")
    )
)]
async fn get_person(Path((id, name)): Path<(String, String)>) {}
```

Previously this was a compile error and Path parameters needed to be
defined via `IntoParams` type.
  • Loading branch information
juhaku authored Dec 5, 2022
1 parent fdd244c commit 0cf8eae
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 22 deletions.
52 changes: 41 additions & 11 deletions utoipa-gen/src/ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,30 +179,52 @@ pub mod fn_arg {
#[cfg_attr(feature = "debug", derive(Debug))]
pub struct FnArg<'a> {
pub(super) ty: TypeTree<'a>,
pub(super) name: &'a Ident,
pub(super) arg_type: FnArgType<'a>,
}

impl<'a> From<(TypeTree<'a>, &'a Ident)> for FnArg<'a> {
fn from((ty, name): (TypeTree<'a>, &'a Ident)) -> Self {
Self { ty, name }
#[cfg_attr(feature = "debug", derive(Debug))]
#[derive(PartialEq, Eq, PartialOrd, Ord)]
pub enum FnArgType<'t> {
Single(&'t Ident),
Tuple(Vec<&'t Ident>),
}

impl FnArgType<'_> {
/// Get best effor name `Ident` for the type. For `FnArgType::Tuple` types it will take the first one
/// from `Vec`.
#[cfg(feature = "rocket_extras")]
pub(super) fn get_name(&self) -> &Ident {
match self {
Self::Single(ident) => ident,
// perform best effort name, by just taking the first one from the list
Self::Tuple(tuple) => tuple
.first()
.expect("Expected at least one argument in FnArgType::Tuple"),
}
}
}

impl<'a> From<(TypeTree<'a>, FnArgType<'a>)> for FnArg<'a> {
fn from((ty, arg_type): (TypeTree<'a>, FnArgType<'a>)) -> Self {
Self { ty, arg_type }
}
}

impl<'a> Ord for FnArg<'a> {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.name.cmp(other.name)
self.arg_type.cmp(&other.arg_type)
}
}

impl<'a> PartialOrd for FnArg<'a> {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
self.name.partial_cmp(other.name)
self.arg_type.partial_cmp(&other.arg_type)
}
}

impl<'a> PartialEq for FnArg<'a> {
fn eq(&self, other: &Self) -> bool {
self.ty == other.ty && self.name == other.name
self.ty == other.ty && self.arg_type == other.arg_type
}
}

Expand All @@ -214,18 +236,26 @@ pub mod fn_arg {
.map(|arg| {
let pat_type = get_fn_arg_pat_type(arg);

let arg_name = get_pat_ident(pat_type.pat.as_ref());
let arg_name = get_pat_fn_arg_type(pat_type.pat.as_ref());
(TypeTree::from_type(&pat_type.ty), arg_name)
})
.map(FnArg::from)
}

#[inline]
fn get_pat_ident(pat: &Pat) -> &Ident {
fn get_pat_fn_arg_type(pat: &Pat) -> FnArgType {
let arg_name = match pat {
syn::Pat::Ident(ident) => &ident.ident,
syn::Pat::Ident(ident) => FnArgType::Single(&ident.ident),
syn::Pat::Tuple(tuple) => {
FnArgType::Tuple(tuple.elems.iter().map(|item| {
match item {
syn::Pat::Ident(ident) => &ident.ident,
_ => abort!(item, "expected syn::Ident in get_pat_fn_arg_type Pat::Tuple")
}
}).collect::<Vec<_>>())
},
syn::Pat::TupleStruct(tuple_struct) => {
get_pat_ident(tuple_struct.pat.elems.first().as_ref().expect(
get_pat_fn_arg_type(tuple_struct.pat.elems.first().as_ref().expect(
"PatTuple expected to have at least one element, cannot get fn argument",
))
}
Expand Down
53 changes: 49 additions & 4 deletions utoipa-gen/src/ext/axum.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
use std::borrow::Cow;

use syn::{punctuated::Punctuated, token::Comma};

use crate::component::TypeTree;

use super::{
fn_arg::{self, FnArg},
ArgumentResolver, PathOperations,
fn_arg::{self, FnArg, FnArgType},
ArgumentResolver, PathOperations, ValueArgument,
};

// axum framework is only able to resolve handler function arguments.
Expand All @@ -15,12 +19,12 @@ impl ArgumentResolver for PathOperations {
Option<Vec<super::ValueArgument<'_>>>,
Option<Vec<super::IntoParamsType<'_>>>,
) {
let (into_params_args, _): (Vec<FnArg>, Vec<FnArg>) = fn_arg::get_fn_args(args)
let (into_params_args, value_args): (Vec<FnArg>, Vec<FnArg>) = fn_arg::get_fn_args(args)
.into_iter()
.partition(fn_arg::is_into_params);

(
None,
Some(get_value_arguments(value_args).collect()),
Some(
into_params_args
.into_iter()
Expand All @@ -31,3 +35,44 @@ impl ArgumentResolver for PathOperations {
)
}
}

fn get_value_arguments(value_args: Vec<FnArg>) -> impl Iterator<Item = super::ValueArgument<'_>> {
value_args
.into_iter()
.filter(|arg| arg.ty.is("Path"))
.flat_map(|path_arg| match path_arg.arg_type {
FnArgType::Single(_) => path_arg
.ty
.children
.expect("Path argument must have children")
.into_iter()
.map(|ty| to_value_argument(None, ty))
.collect::<Vec<_>>(),
FnArgType::Tuple(tuple) => tuple
.iter()
.zip(
path_arg
.ty
.children
.expect("Path argument must have children")
.into_iter()
.flat_map(|child| {
child
.children
.expect("ValueType::Tuple will always have children")
}),
)
.map(|(name, ty)| to_value_argument(Some(Cow::Owned(name.to_string())), ty))
.collect::<Vec<_>>(),
})
}

fn to_value_argument<'a>(name: Option<Cow<'a, str>>, ty: TypeTree<'a>) -> ValueArgument<'a> {
ValueArgument {
name,
is_array: false,
is_option: false,
type_path: ty.path,
argument_in: super::ArgumentIn::Path,
}
}
10 changes: 5 additions & 5 deletions utoipa-gen/src/ext/rocket.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ fn to_value_arg((arg, argument_in): (FnArg, ArgumentIn)) -> ValueArgument {
ValueArgument {
type_path: get_value_type(arg.ty),
argument_in,
name: Some(Cow::Owned(arg.name.to_string())),
name: Some(Cow::Owned(arg.arg_type.get_name().to_string())),
is_array: is_vec,
is_option,
}
Expand All @@ -94,14 +94,14 @@ fn with_parameter_in(
move |arg: FnArg| {
let parameter_in = named_args.iter().find_map(|macro_arg| match macro_arg {
MacroArg::Path(path) => {
if arg.name == &*path.name {
if arg.arg_type.get_name() == &*path.name {
Some(quote! { || Some(utoipa::openapi::path::ParameterIn::Path) })
} else {
None
}
}
MacroArg::Query(query) => {
if arg.name == &*query.name {
if arg.arg_type.get_name() == &*query.name {
Some(quote! { || Some(utoipa::openapi::path::ParameterIn::Query) })
} else {
None
Expand All @@ -117,14 +117,14 @@ fn with_argument_in(named_args: &[MacroArg]) -> impl Fn(FnArg) -> Option<(FnArg,
move |arg: FnArg| {
let argument_in = named_args.iter().find_map(|macro_arg| match macro_arg {
MacroArg::Path(path) => {
if arg.name == &*path.name {
if arg.arg_type.get_name() == &*path.name {
Some(ArgumentIn::Path)
} else {
None
}
}
MacroArg::Query(query) => {
if arg.name == &*query.name {
if arg.arg_type.get_name() == &*query.name {
Some(ArgumentIn::Query)
} else {
None
Expand Down
32 changes: 30 additions & 2 deletions utoipa-gen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -837,8 +837,36 @@ pub fn derive_to_schema(input: TokenStream) -> TokenStream {
///
/// # axum_extras suppport for axum
///
/// **axum_extras** feature enhances [`IntoParams` derive][into_params_derive] functionality by automatically resolving _`parameter_in`_ from
/// _`Path<...>`_ or _`Query<...>`_ handler function arguments.
/// **axum_extras** feature enhances parameter support for path operation in following ways.
///
/// 1. It allows users to use tuple style path parameters e.g. _`Path((id, name)): Path<(i32, String)>`_ and resolves
/// parameter names and types from it.
/// 2. It enhances [`IntoParams` derive][into_params_derive] functionality by automatically resolving _`parameter_in`_ from
/// _`Path<...>`_ or _`Query<...>`_ handler function arguments.
///
/// _**Resole path argument types from tuple style handler arguments.**_
/// ```rust
/// # use axum::extract::Path;
/// /// Get todo by id and name.
/// #[utoipa::path(
/// get,
/// path = "/todo/{id}",
/// params(
/// ("id", description = "Todo id"),
/// ("name", description = "Todo name")
/// ),
/// responses(
/// (status = 200, description = "Get todo success", body = String)
/// )
/// )]
/// async fn get_todo(
/// Path((id, name)): Path<(i32, String)>
/// ) -> String {
/// String::new()
/// }
/// ```
///
/// _**Use `IntoParams` to resovle query parmaeters.**_
/// ```rust
/// # use serde::Deserialize;
/// # use utoipa::IntoParams;
Expand Down
52 changes: 52 additions & 0 deletions utoipa-gen/tests/path_derive_axum_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,58 @@ fn derive_path_params_into_params_axum() {
)
}

#[test]
fn get_todo_with_path_tuple() {
#[utoipa::path(
get,
path = "/person/{id}/{name}",
params(
("id", description = "Person id"),
("name", description = "Person name")
),
responses(
(status = 200, description = "success response")
)
)]
#[allow(unused)]
async fn get_person(Path((id, name)): Path<(String, String)>) {}

#[derive(OpenApi)]
#[openapi(paths(get_person))]
struct ApiDoc;

let doc = serde_json::to_value(ApiDoc::openapi()).unwrap();
let parameters = doc
.pointer("/paths/~1person~1{id}~1{name}/get/parameters")
.unwrap();

assert_json_eq!(
parameters,
&json!([
{
"description": "Person id",
"in": "path",
"name": "id",
"deprecated": false,
"required": true,
"schema": {
"type": "string"
},
},
{
"description": "Person name",
"in": "path",
"name": "name",
"deprecated": false,
"required": true,
"schema": {
"type": "string",
},
},
])
)
}

#[test]
fn get_todo_with_extension() {
struct Todo {
Expand Down

0 comments on commit 0cf8eae

Please sign in to comment.