Skip to content

Commit

Permalink
Add IntoParams trait derive (#100)
Browse files Browse the repository at this point in the history
* Add IntoParams trait derive implementation with tests. Previously only path parameters
  with tuple or primitive types where supported. This enhances actix-web parameter
  resolving to support structs to resolve path and query parameters making params
  declaration within utoipa::path obsolete.
* Add documentation of IntoParams feature with examples.
  • Loading branch information
juhaku authored Apr 23, 2022
1 parent 6cec102 commit 0bd2bf4
Show file tree
Hide file tree
Showing 8 changed files with 401 additions and 30 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ jobs:
cargo test --test component_derive_test --features chrono_types_with_format
cargo test --test path_derive_rocket --features rocket_extras,json
elif [[ "${{ matrix.testset }}" == "utoipa-gen" ]] && [[ ${{ steps.changes.outputs.gen_changed }} == true ]]; then
cargo test -p utoipa-gen --features actix_extras
cargo test -p utoipa-gen --features utoipa/actix_extras
elif [[ "${{ matrix.testset }}" == "utoipa-swagger-ui" ]] && [[ ${{ steps.changes.outputs.swagger_changed }} == true ]]; then
cargo test -p utoipa-swagger-ui --features actix-web,rocket
fi
78 changes: 74 additions & 4 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,6 @@
pub mod openapi;

use openapi::path::{Parameter, ParameterBuilder};
pub use utoipa_gen::*;

/// Trait for implementing OpenAPI specification in Rust.
Expand Down Expand Up @@ -463,11 +462,82 @@ pub trait Modify {
fn modify(&self, openapi: &mut openapi::OpenApi);
}

/// Trait used to convert implementing type to OpenAPI parameters for **actix-web** framework.
///
/// This trait is [deriveable][derive] for structs which are used to describe `path` or `query` parameters.
/// For more details of `#[derive(IntoParams)]` refer to [derive documentation][derive].
///
/// # Examples
///
/// Derive [`IntoParams`] implementation. This example will fail to compile because [`IntoParams`] cannot
/// be used alone and it need to be used together with endpoint using the params as well. See
/// [derive documentation][derive] for more details.
/// ```compile_fail
/// use utoipa::{IntoParams};
///
/// #[derive(IntoParams)]
/// struct PetParams {
/// /// Id of pet
/// id: i64,
/// /// Name of pet
/// name: String,
/// }
/// ```
///
/// Roughly equal manual implementation of [`IntoParams`] trait.
/// ```rust
/// # struct PetParams {
/// # /// Id of pet
/// # id: i64,
/// # /// Name of pet
/// # name: String,
/// # }
/// impl utoipa::IntoParams for PetParams {
/// fn into_params() -> Vec<utoipa::openapi::path::Parameter> {
/// vec![
/// utoipa::openapi::path::ParameterBuilder::new()
/// .name("id")
/// .required(utoipa::openapi::Required::True)
/// .parameter_in(utoipa::openapi::path::ParameterIn::Path)
/// .description(Some("Id of pet"))
/// .schema(Some(
/// utoipa::openapi::PropertyBuilder::new()
/// .component_type(utoipa::openapi::ComponentType::Integer)
/// .format(Some(utoipa::openapi::ComponentFormat::Int64)),
/// ))
/// .build(),
/// utoipa::openapi::path::ParameterBuilder::new()
/// .name("name")
/// .required(utoipa::openapi::Required::True)
/// .parameter_in(utoipa::openapi::path::ParameterIn::Path)
/// .description(Some("Name of pet"))
/// .schema(Some(
/// utoipa::openapi::PropertyBuilder::new()
/// .component_type(utoipa::openapi::ComponentType::String),
/// ))
/// .build(),
/// ]
/// }
/// }
/// ```
/// [derive]: derive.IntoParams.html
#[cfg(feature = "actix_extras")]
pub trait IntoParams {
fn default_parameters_in() -> Option<openapi::path::ParameterIn> {
/// Provide [`Vec`] of [`openapi::path::Parameter`]s to caller. The result is used in `utoipa-gen` library to
/// provide OpenAPI parameter information for the endpoint using the parameters.
fn into_params() -> Vec<openapi::path::Parameter>;
}

/// Internal trait used to provide [`ParameterIn`] definition for implementer type.
///
/// This is typically used in tandem with [`IntoParams`] trait and only from `utoipa-gen` library.
/// In manual implementation there is typially never a need to implement this trait since
/// manual implementations can directly define the [`ParameterIn`] definition they see fit to the purpose.
#[cfg(feature = "actix_extras")]
#[doc(hidden)]
pub trait ParameterIn {
/// Provide [`ParameterIn`] declaration for caller. Default implementation returns [`None`].
fn parameter_in() -> Option<openapi::path::ParameterIn> {
None
}

fn into_params() -> Vec<Parameter>;
}
82 changes: 71 additions & 11 deletions tests/path_derive_actix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -319,16 +319,14 @@ fn path_with_struct_variables_with_into_params() {

impl IntoParams for Person {
fn into_params() -> Vec<Parameter> {
let parameter_in = Self::default_parameters_in().unwrap_or_default();

vec![
ParameterBuilder::new()
.name("name")
.schema(Some(
PropertyBuilder::new()
.component_type(utoipa::openapi::ComponentType::String),
))
.parameter_in(parameter_in.clone())
.parameter_in(ParameterIn::Path)
.build(),
ParameterBuilder::new()
.name("id")
Expand All @@ -337,7 +335,7 @@ fn path_with_struct_variables_with_into_params() {
.component_type(utoipa::openapi::ComponentType::Integer)
.format(Some(ComponentFormat::Int64)),
))
.parameter_in(parameter_in)
.parameter_in(ParameterIn::Path)
.build(),
]
}
Expand All @@ -350,19 +348,13 @@ fn path_with_struct_variables_with_into_params() {
}

impl IntoParams for Filter {
fn default_parameters_in() -> Option<utoipa::openapi::path::ParameterIn> {
Some(ParameterIn::Query)
}

fn into_params() -> Vec<Parameter> {
let parameter_in = Self::default_parameters_in().unwrap_or_default();

vec![ParameterBuilder::new()
.name("age")
.schema(Some(Array::new(
PropertyBuilder::new().component_type(utoipa::openapi::ComponentType::String),
)))
.parameter_in(parameter_in)
.parameter_in(ParameterIn::Query)
.build()]
}
}
Expand Down Expand Up @@ -407,6 +399,74 @@ fn path_with_struct_variables_with_into_params() {
}
}

#[test]
fn derive_path_with_struct_variables_with_into_params() {
use actix_web::{get, HttpResponse, Responder};
use serde_json::json;

#[derive(Deserialize, IntoParams)]
#[allow(unused)]
struct Person {
/// Id of person
id: i64,
/// Name of person
name: String,
}

#[derive(Deserialize, IntoParams)]
#[allow(unused)]
struct Filter {
/// Age filter for user
#[deprecated]
age: Option<Vec<String>>,
}

#[utoipa::path(
responses(
(status = 200, description = "success response")
)
)]
#[get("/foo/{id}/{name}")]
#[allow(unused)]
async fn get_foo(person: Path<Person>, query: Query<Filter>) -> impl Responder {
HttpResponse::Ok().json(json!({ "id": "foo" }))
}

#[derive(OpenApi, Default)]
#[openapi(handlers(get_foo))]
struct ApiDoc;

let doc = serde_json::to_value(ApiDoc::openapi()).unwrap();
let parameters = common::get_json_path(&doc, "paths./foo/{id}/{name}.get.parameters");

common::assert_json_array_len(parameters, 3);
assert_value! {parameters=>
"[0].in" = r#""path""#, "Parameter in"
"[0].name" = r#""id""#, "Parameter name"
"[0].description" = r#""Id of person""#, "Parameter description"
"[0].required" = r#"true"#, "Parameter required"
"[0].deprecated" = r#"null"#, "Parameter deprecated"
"[0].schema.type" = r#""integer""#, "Parameter schema type"
"[0].schema.format" = r#""int64""#, "Parameter schema format"

"[1].in" = r#""path""#, "Parameter in"
"[1].name" = r#""name""#, "Parameter name"
"[1].description" = r#""Name of person""#, "Parameter description"
"[1].required" = r#"true"#, "Parameter required"
"[1].deprecated" = r#"null"#, "Parameter deprecated"
"[1].schema.type" = r#""string""#, "Parameter schema type"
"[1].schema.format" = r#"null"#, "Parameter schema format"

"[2].in" = r#""query""#, "Parameter in"
"[2].name" = r#""age""#, "Parameter name"
"[2].description" = r#""Age filter for user""#, "Parameter description"
"[2].required" = r#"false"#, "Parameter required"
"[2].deprecated" = r#"true"#, "Parameter deprecated"
"[2].schema.type" = r#""array""#, "Parameter schema type"
"[2].schema.items.type" = r#""string""#, "Parameter items schema type"
}
}

macro_rules! test_derive_path_operations {
( $( $name:ident, $mod:ident: $operation:ident)* ) => {
$(
Expand Down
1 change: 1 addition & 0 deletions utoipa-gen/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ lazy_static = { version = "1.4", optional = true }
[dev-dependencies]
utoipa = { path = ".." }
serde_json = "1"
serde = "1"
actix-web = { version = "4" }

[features]
Expand Down
18 changes: 9 additions & 9 deletions utoipa-gen/src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,7 @@ impl ToTokens for ComplexEnum<'_> {
}
}

fn get_deprecated(attributes: &[Attribute]) -> Option<Deprecated> {
pub(crate) fn get_deprecated(attributes: &[Attribute]) -> Option<Deprecated> {
attributes.iter().find_map(|attribute| {
if *attribute.path.get_ident().unwrap() == "deprecated" {
Some(Deprecated::True)
Expand All @@ -423,15 +423,15 @@ fn get_deprecated(attributes: &[Attribute]) -> Option<Deprecated> {
#[derive(PartialEq)]
#[cfg_attr(feature = "debug", derive(Debug))]
/// Linked list of implementing types of a field in a struct.
struct ComponentPart<'a> {
ident: &'a Ident,
value_type: ValueType,
generic_type: Option<GenericType>,
child: Option<Rc<ComponentPart<'a>>>,
pub struct ComponentPart<'a> {
pub ident: &'a Ident,
pub value_type: ValueType,
pub generic_type: Option<GenericType>,
pub child: Option<Rc<ComponentPart<'a>>>,
}

impl<'a> ComponentPart<'a> {
fn from_type(ty: &'a Type) -> ComponentPart<'a> {
pub fn from_type(ty: &'a Type) -> ComponentPart<'a> {
ComponentPart::from_type_path(
match ty {
Type::Path(path) => path,
Expand Down Expand Up @@ -544,14 +544,14 @@ impl<'a> ComponentPart<'a> {

#[cfg_attr(feature = "debug", derive(Debug))]
#[derive(Clone, Copy, PartialEq)]
enum ValueType {
pub enum ValueType {
Primitive,
Object,
}

#[cfg_attr(feature = "debug", derive(Debug))]
#[derive(PartialEq, Clone, Copy)]
enum GenericType {
pub enum GenericType {
Vec,
Map,
Option,
Expand Down
12 changes: 9 additions & 3 deletions utoipa-gen/src/ext/actix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,22 @@ impl PathOperations {
arguments: I,
) -> impl Iterator<Item = Argument<'a>> {
arguments.into_iter().map(|path_arg| {
let ty = match path_arg {
Arg::Path(arg) => arg,
Arg::Query(arg) => arg,
let (ty, parameter_in) = match path_arg {
Arg::Path(arg) => (arg, quote! { utoipa::openapi::path::ParameterIn::Path }),
Arg::Query(arg) => (arg, quote! { utoipa::openapi::path::ParameterIn::Query }),
};

let assert_ty = format_ident!("_Assert{}", &ty);
Argument::TokenStream(quote! {
{
struct #assert_ty where #ty : utoipa::IntoParams;

impl utoipa::ParameterIn for #ty {
fn parameter_in() -> Option<utoipa::openapi::path::ParameterIn> {
Some(#parameter_in)
}
}

<#ty>::into_params()
}
})
Expand Down
Loading

0 comments on commit 0bd2bf4

Please sign in to comment.