Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions examples/simple-x-extensions/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,17 @@ impl utoipa::Modify for ApiModify {
}
}

#[derive(Debug, Clone, utoipa::IntoParams)]
#[into_params(parameter_in = Query, extensions(("x-on-both-struct-params" = json!(true))))]
pub struct ParamStruct {
/// Another param.
pub param_1: i32,

/// Yet another param.
#[param(extensions(("x-on-one-param" = json!({ "key": "value" }))))]
pub param_2: String,
}

#[utoipa::path(
get,
path = "/openapi",
Expand All @@ -168,6 +179,7 @@ impl utoipa::Modify for ApiModify {
("x-ext-macro" = json!( "[Macro] openapi>Paths>PathItem>Operation>Parameters>item" ) )
)
),
ParamStruct
),
responses(
( status = 200,
Expand Down
1 change: 1 addition & 0 deletions utoipa-gen/src/component/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,7 @@ impl_feature_into_inner! {
attributes::Bound,
attributes::Ignore,
attributes::NoRecursion,
attributes::Extensions,
validation::MultipleOf,
validation::Maximum,
validation::Minimum,
Expand Down
6 changes: 6 additions & 0 deletions utoipa-gen/src/component/features/attributes/extensions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ impl_feature! {
}
}

impl Extensions {
pub fn merge(&mut self, extensions: Extensions) {
self.extensions.extend(extensions.extensions);
}
}

impl Parse for Extensions {
fn parse(input: syn::parse::ParseStream, _: proc_macro2::Ident) -> syn::Result<Self> {
syn::parse::Parse::parse(input)
Expand Down
30 changes: 27 additions & 3 deletions utoipa-gen/src/component/into_params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ use crate::{
features::{
self,
attributes::{
AdditionalProperties, AllowReserved, Example, Explode, Format, Ignore, Inline,
IntoParamsNames, Nullable, ReadOnly, Rename, RenameAll, SchemaWith, Style,
AdditionalProperties, AllowReserved, Example, Explode, Extensions, Format, Ignore,
Inline, IntoParamsNames, Nullable, ReadOnly, Rename, RenameAll, SchemaWith, Style,
WriteOnly, XmlAttr,
},
validation::{
Expand Down Expand Up @@ -49,7 +49,8 @@ impl Parse for IntoParamsFeatures {
input as Style,
features::attributes::ParameterIn,
IntoParamsNames,
RenameAll
RenameAll,
Extensions
)))
}
}
Expand Down Expand Up @@ -106,6 +107,7 @@ impl ToTokensDiagnostics for IntoParams {
let style = pop_feature!(into_params_features => Feature::Style(_));
let parameter_in = pop_feature!(into_params_features => Feature::ParameterIn(_));
let rename_all = pop_feature!(into_params_features => Feature::RenameAll(_));
let extensions = pop_feature!(into_params_features => Feature::Extensions(_));

let params = self
.get_struct_fields(&names.as_ref())?
Expand Down Expand Up @@ -148,6 +150,7 @@ impl ToTokensDiagnostics for IntoParams {
}),
style: &style,
parameter_in: &parameter_in,
extensions: &extensions,
name,
}, &serde_container, &self.generics)?;

Expand Down Expand Up @@ -264,6 +267,8 @@ pub struct FieldParamContainerAttributes<'a> {
parameter_in: &'a Option<Feature>,
/// Custom rename all if serde attribute is not present.
rename_all: Option<&'a RenameAll>,
/// See [`IntoParamsAttr::extensions`].
extensions: &'a Option<Feature>,
}

struct FieldFeatures(Vec<Feature>);
Expand All @@ -282,6 +287,7 @@ impl Parse for FieldFeatures {
Explode,
SchemaWith,
component::features::attributes::Required,
Extensions,
// param schema features
Inline,
Format,
Expand Down Expand Up @@ -353,6 +359,8 @@ impl Param {
.map(|rename_all| rename_all.as_rename_rule()));
let name = super::rename::<FieldRename>(name, rename_to, rename_all)
.unwrap_or(Cow::Borrowed(name));
let extensions =
pop_feature!(param_features => Feature::Extensions(_) as Option<Extensions>);
let type_tree = TypeTree::from_type(&field.ty)?;

tokens.extend(quote! { utoipa::openapi::path::ParameterBuilder::new()
Expand All @@ -372,6 +380,10 @@ impl Param {
tokens.extend(quote! { .deprecated(Some(#deprecated)) });
}

if let Some(extensions) = extensions {
tokens.extend(quote! { .extensions(Some(#extensions)) });
}

let schema_with = pop_feature!(param_features => Feature::SchemaWith(_));
if let Some(schema_with) = schema_with {
let schema_with = crate::as_tokens_or_diagnostics!(&schema_with);
Expand Down Expand Up @@ -466,6 +478,18 @@ impl Param {
};
}

if let Some(Feature::Extensions(ref extensions)) = container_attributes.extensions {
// Merge container-level exensions with field-level extensions if present.
if let Some(Feature::Extensions(ref mut field_extensions)) = field_features
.iter_mut()
.find(|feature| matches!(&feature, Feature::Extensions(_)))
{
field_extensions.merge(extensions.clone());
} else {
field_features.push(Feature::Extensions(extensions.clone()));
}
}

Ok(field_features.into_iter().fold(
(Vec::<Feature>::new(), Vec::<Feature>::new()),
|(mut schema_features, mut param_features), feature| {
Expand Down
3 changes: 3 additions & 0 deletions utoipa-gen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2286,6 +2286,7 @@ pub fn openapi(input: TokenStream) -> TokenStream {
/// supplied, then the value is determined by the `parameter_in_provider` in
/// [`IntoParams::into_params()`](trait.IntoParams.html#tymethod.into_params).
/// * `rename_all = ...` Can be provided to alternatively to the serde's `rename_all` attribute. Effectively provides same functionality.
/// * `extensions(...)` List of extensions applied to all parameters in the struct. Additive with extensions specfied in field-level `#[param(...)]` attributes.
///
/// Use `names` to define name for single unnamed argument.
/// ```rust
Expand Down Expand Up @@ -2384,6 +2385,8 @@ pub fn openapi(input: TokenStream) -> TokenStream {
/// * `ignore` or `ignore = ...` Can be used to skip the field from being serialized to OpenAPI schema. It accepts either a literal `bool` value
/// or a path to a function that returns `bool` (`Fn() -> bool`).
///
/// * `extensions(...)` List of extensions local to the parameter. Additive with extensions specfied in struct-level `#[into_params(...)]` attributes.
///
/// #### Field nullability and required rules
///
/// Same rules for nullability and required status apply for _`IntoParams`_ field attributes as for
Expand Down
57 changes: 57 additions & 0 deletions utoipa-gen/tests/path_derive_actix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,63 @@ fn derive_into_params_in_another_module() {
}
}

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

#[derive(IntoParams, Deserialize)]
#[into_params(extensions(("x-some-ext" = json!(true))))]
#[allow(unused)]
struct Person {
/// Name of person
name: String,
/// City of residence
#[param(extensions(("x-other-ext" = json!(1))))]
city: Option<String>,
}

/// Get person by id
#[utoipa::path(
params(
Person
),
responses(
(status = 200, description = "success response")
)
)]
#[get("/person")]
async fn get_person(person: Query<Person>) -> impl Responder {
HttpResponse::Ok()
}

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

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

common::assert_json_array_len(parameters, 2);
assert_value! {parameters=>
"[0].in" = r#""query""#, "Parameter in"
"[0].name" = r#""name""#, "Parameter name"
"[0].description" = r#""Name of person""#, "Parameter description"
"[0].required" = r#"true"#, "Parameter required"
"[0].schema.type" = r#""string""#, "Parameter schema type"
"[0].schema.format" = r#"null"#, "Parameter schema format"
"[0].x-some-ext" = r#"true"#, "Parameter x-some-ext"

"[1].in" = r#""query""#, "Parameter in"
"[1].name" = r#""city""#, "Parameter name"
"[1].description" = r#""City of residence""#, "Parameter description"
"[1].required" = r#"false"#, "Parameter required"
"[1].schema.type" = r#"["string","null"]"#, "Parameter schema type"
"[1].schema.format" = r#"null"#, "Parameter schema format"
"[1].x-some-ext" = r#"true"#, "Parameter x-some-ext"
"[1].x-other-ext" = r#"1"#, "Parameter x-other-ext"
};
}

#[test]
fn path_with_all_args() {
#![allow(unused)]
Expand Down
56 changes: 56 additions & 0 deletions utoipa-gen/tests/path_derive_axum_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ use insta::assert_json_snapshot;
use serde::Deserialize;
use utoipa::{IntoParams, OpenApi};

mod common;

#[test]
fn derive_path_params_into_params_axum() {
#[derive(Deserialize, IntoParams)]
Expand Down Expand Up @@ -437,3 +439,57 @@ fn path_derive_inline_with_tuple() {

assert_json_snapshot!(value);
}

#[test]
fn derive_into_params_with_extensions() {
#[derive(IntoParams)]
#[into_params(extensions(("x-some-ext" = json!(true))))]
#[allow(unused)]
struct Person {
/// Name of person
name: String,
/// City of residence
#[param(extensions(("x-other-ext" = json!(1))))]
city: Option<String>,
}

/// Get person by id
#[utoipa::path(
get,
path = "/person",
params(
Person
),
responses(
(status = 200, description = "success response")
)
)]
async fn get_person(person: Query<Person>) {}

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

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

common::assert_json_array_len(parameters, 2);
assert_value! {parameters=>
"[0].in" = r#""query""#, "Parameter in"
"[0].name" = r#""name""#, "Parameter name"
"[0].description" = r#""Name of person""#, "Parameter description"
"[0].required" = r#"true"#, "Parameter required"
"[0].schema.type" = r#""string""#, "Parameter schema type"
"[0].schema.format" = r#"null"#, "Parameter schema format"
"[0].x-some-ext" = r#"true"#, "Parameter x-some-ext"

"[1].in" = r#""query""#, "Parameter in"
"[1].name" = r#""city""#, "Parameter name"
"[1].description" = r#""City of residence""#, "Parameter description"
"[1].required" = r#"false"#, "Parameter required"
"[1].schema.type" = r#""string""#, "Parameter schema type"
"[1].schema.format" = r#"null"#, "Parameter schema format"
"[1].x-some-ext" = r#"true"#, "Parameter x-some-ext"
"[1].x-other-ext" = r#"1"#, "Parameter x-other-ext"
};
}
58 changes: 58 additions & 0 deletions utoipa-gen/tests/path_derive_rocket.rs
Original file line number Diff line number Diff line change
Expand Up @@ -565,3 +565,61 @@ fn derive_rocket_path_with_query_params_in_option() {

assert_json_snapshot!(value);
}

#[test]
fn derive_rocket_path_with_param_extensions() {
#[derive(FromForm, IntoParams)]
#[into_params(parameter_in = Query, style = Form, extensions(("x-some-ext" = json!(true))))]
#[allow(unused)]
struct Person {
/// Name of person
name: String,
/// City of residence
#[param(extensions(("x-other-ext" = json!(1))))]
city: Option<String>,
}

/// Get person by id
#[utoipa::path(
params(
Person
),
responses(
(status = 200, description = "success response")
)
)]
#[get("/person?<person..>")]
async fn get_person(person: Person) -> String {
String::new()
}

let operation = __path_get_person::operation();
let value = serde_json::to_value(&operation).expect("operation is JSON serializable");

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

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

common::assert_json_array_len(parameters, 2);
assert_value! {parameters=>
"[0].in" = r#""query""#, "Parameter in"
"[0].name" = r#""name""#, "Parameter name"
"[0].description" = r#""Name of person""#, "Parameter description"
"[0].required" = r#"true"#, "Parameter required"
"[0].schema.type" = r#""string""#, "Parameter schema type"
"[0].schema.format" = r#"null"#, "Parameter schema format"
"[0].x-some-ext" = r#"true"#, "Parameter x-some-ext"

"[1].in" = r#""query""#, "Parameter in"
"[1].name" = r#""city""#, "Parameter name"
"[1].description" = r#""City of residence""#, "Parameter description"
"[1].required" = r#"false"#, "Parameter required"
"[1].schema.type" = r#""string""#, "Parameter schema type"
"[1].schema.format" = r#"null"#, "Parameter schema format"
"[1].x-some-ext" = r#"true"#, "Parameter x-some-ext"
"[1].x-other-ext" = r#"1"#, "Parameter x-other-ext"
};
}
Loading