Skip to content

Commit

Permalink
Add value_type for IntoParams (#203)
Browse files Browse the repository at this point in the history
Add `value_type` attribute to `IntoParams` to allow users to override
parameter type with a custom type. Enhance `value_type` on for
`IntoParams` to allow all Rust types to be used as override type for
parameters.
  • Loading branch information
juhaku authored Jul 2, 2022
1 parent 58afc12 commit 7178d07
Show file tree
Hide file tree
Showing 7 changed files with 397 additions and 97 deletions.
4 changes: 4 additions & 0 deletions tests/common.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
#![cfg(feature = "serde_json")]
use serde_json::Value;

#[deprecated(
since = "2.0.0",
note = "Favor serde native `.pointer(...)` function over custom json path function"
)]
pub fn get_json_path<'a>(value: &'a Value, path: &str) -> &'a Value {
path.split('.').into_iter().fold(value, |acc, fragment| {
let value = if fragment.starts_with('[') && fragment.ends_with(']') {
Expand Down
138 changes: 138 additions & 0 deletions tests/path_derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,144 @@ fn derive_path_params_intoparams() {
)
}

#[test]
fn derive_path_params_into_params_with_value_type() {
use utoipa::OpenApi;

#[derive(Component)]
struct Foo {
#[allow(unused)]
value: String,
}

#[derive(IntoParams)]
#[into_params(parameter_in = Query)]
#[allow(unused)]
struct Filter {
#[param(value_type = i64, style = Simple)]
id: String,
#[param(value_type = Any)]
another_id: String,
#[param(value_type = Vec<Vec<String>>)]
value1: Vec<i64>,
#[param(value_type = Vec<String>)]
value2: Vec<i64>,
#[param(value_type = Option<String>)]
value3: i64,
#[param(value_type = Option<Any>)]
value4: i64,
#[param(value_type = Vec<Any>)]
value5: i64,
#[param(value_type = Vec<Foo>)]
value6: i64,
}

#[utoipa::path(
get,
path = "foo",
responses(
(status = 200, description = "success response")
),
params(
Filter
)
)]
#[allow(unused)]
fn get_foo(query: Filter) {}

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

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

assert_json_eq!(
parameters,
json!([{
"in": "query",
"name": "id",
"required": true,
"style": "simple",
"schema": {
"format": "int64",
"type": "integer"
}
},
{
"in": "query",
"name": "another_id",
"required": true,
"schema": {
"type": "object"
}
},
{
"in": "query",
"name": "value1",
"required": true,
"schema": {
"items": {
"items": {
"type": "string"
},
"type": "array"
},
"type": "array"
}
},
{
"in": "query",
"name": "value2",
"required": true,
"schema": {
"items": {
"type": "string"
},
"type": "array"
}
},
{
"in": "query",
"name": "value3",
"required": false,
"schema": {
"type": "string"
}
},
{
"in": "query",
"name": "value4",
"required": false,
"schema": {
"type": "object"
}
},
{
"in": "query",
"name": "value5",
"required": true,
"schema": {
"items": {
"type": "object"
},
"type": "array"
}
},
{
"in": "query",
"name": "value6",
"required": true,
"schema": {
"items": {
"$ref": "#/components/schemas/Foo"
},
"type": "array"
}
}])
)
}

#[cfg(feature = "uuid")]
#[test]
fn derive_path_with_uuid() {
Expand Down
96 changes: 75 additions & 21 deletions utoipa-gen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -932,10 +932,15 @@ pub fn openapi(input: TokenStream) -> TokenStream {
/// The following attributes are available for use in the `#[param(...)]` on struct fields:
///
/// * `style = ...` Defines how the parameter is serialized by [`ParameterStyle`][style]. Default values are based on _`parameter_in`_ attribute.
/// * `explode` Defines whether new _`parameter=value`_ is created for each parameter withing _`object`_ or _`array`_.
/// * `explode` Defines whether new _`parameter=value`_ pair is created for each parameter withing _`object`_ or _`array`_.
/// * `allow_reserved` Defines whether reserved characters _`:/?#[]@!$&'()*+,;=`_ is allowed within value.
/// * `example = ...` Can be literal value, method reference or _`json!(...)`_. [^json] Given example
/// will override any example in underlying parameter type.
/// * `value_type = ...` Can be used to override default type derived from type of the field used in OpenAPI spec.
/// This is useful in cases where the default type does not correspond to the actual type e.g. when
/// any third-party types are used which are not [`Component`][component]s nor [`primitive` types][primitive].
/// Value can be any Rust type what normally could be used to serialize to JSON or custom type such as _`Any`_.
/// _`Any`_ will be rendered as generic OpenAPI object.
/// * `inline` If set, the schema for this field's type needs to be a [`Component`][component], and
/// the component schema definition will be inlined.
///
Expand Down Expand Up @@ -996,7 +1001,7 @@ pub fn openapi(input: TokenStream) -> TokenStream {
/// }
/// ```
///
/// Demonstrate [`IntoParams`][into_params] usage with the `#[param(...)]` container attribute to
/// Demonstrate [`IntoParams`][into_params] usage with the `#[into_params(...)]` container attribute to
/// be used as a path query, and inlining a component query field:
/// ```rust
/// use serde::Deserialize;
Expand All @@ -1010,7 +1015,7 @@ pub fn openapi(input: TokenStream) -> TokenStream {
/// }
///
/// #[derive(Deserialize, IntoParams)]
/// #[param(style = Form, parameter_in = Query)]
/// #[into_params(style = Form, parameter_in = Query)]
/// struct PetQuery {
/// /// Name of pet
/// name: Option<String>,
Expand All @@ -1033,12 +1038,79 @@ pub fn openapi(input: TokenStream) -> TokenStream {
/// // ...
/// }
/// ```
///
/// Override `String` with `i64` using `value_type` attribute.
/// ```rust
/// # use utoipa::IntoParams;
/// #
/// #[derive(IntoParams)]
/// #[into_params(parameter_in = Query)]
/// struct Filter {
/// #[param(value_type = i64)]
/// id: String,
/// }
/// ```
///
/// Override `String` with `Any` using `value_type` attribute. _`Any`_ will render as `type: object` in OpenAPI spec.
/// ```rust
/// # use utoipa::IntoParams;
/// #
/// #[derive(IntoParams)]
/// #[into_params(parameter_in = Query)]
/// struct Filter {
/// #[param(value_type = Any)]
/// id: String,
/// }
/// ```
///
/// You can use a generic type to override the default type of the field.
/// ```rust
/// # use utoipa::IntoParams;
/// #
/// #[derive(IntoParams)]
/// #[into_params(parameter_in = Query)]
/// struct Filter {
/// #[param(value_type = Option<String>)]
/// id: String
/// }
/// ```
///
/// You can even overide a [`Vec`] with another one.
/// ```rust
/// # use utoipa::IntoParams;
/// #
/// #[derive(IntoParams)]
/// #[into_params(parameter_in = Query)]
/// struct Filter {
/// #[param(value_type = Vec<i32>)]
/// id: Vec<String>
/// }
/// ```
///
/// We can override value with another [`Component`][component].
/// ```rust
/// # use utoipa::{IntoParams, Component};
/// #
/// #[derive(Component)]
/// struct Id {
/// value: i64,
/// }
///
/// #[derive(IntoParams)]
/// #[into_params(parameter_in = Query)]
/// struct Filter {
/// #[param(value_type = Id)]
/// id: String
/// }
/// ```
///
/// [component]: trait.Component.html
/// [into_params]: trait.IntoParams.html
/// [path_params]: attr.path.html#params-attributes
/// [struct]: https://doc.rust-lang.org/std/keyword.struct.html
/// [style]: openapi/path/enum.ParameterStyle.html
/// [in_enum]: utoipa/openapi/path/enum.ParameterIn.html
/// [primitive]: https://doc.rust-lang.org/std/primitive/index.html
///
/// [^actix]: Feature **actix_extras** need to be enabled
///
Expand Down Expand Up @@ -1118,24 +1190,6 @@ where
}
}

/// Wrapper for `Ident` type which can be parsed with expression path e.g `path::to::Type`.
/// This is typically used in component `value_type` when type of the field is overridden by the user.
#[cfg_attr(feature = "debug", derive(Debug))]
struct ValueType(ExprPath);

impl ValueType {
/// Get the `Ident` of last segment of the [`syn::ExprPath`].
fn get_ident(&self) -> Option<&Ident> {
self.0.path.segments.last().map(|segment| &segment.ident)
}
}

impl Parse for ValueType {
fn parse(input: ParseStream) -> syn::Result<Self> {
Ok(Self(input.parse()?))
}
}

#[cfg_attr(feature = "debug", derive(Debug))]
enum Deprecated {
True,
Expand Down
37 changes: 15 additions & 22 deletions utoipa-gen/src/path/parameter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,18 +125,7 @@ impl Parse for ParameterValue<'_> {
}

while !input.is_empty() {
let fork = input.fork();

let use_parameter_ext = if fork.peek(syn::Ident) {
let ident = fork.parse::<Ident>().unwrap();
let name = &*ident.to_string();

matches!(name, "style" | "explode" | "allow_reserved" | "example")
} else {
false
};

if use_parameter_ext {
if ParameterExt::is_parameter_ext(&input) {
let ext = parameter
.parameter_ext
.get_or_insert(ParameterExt::default());
Expand Down Expand Up @@ -313,7 +302,6 @@ pub struct ParameterExt {
pub style: Option<ParameterStyle>,
pub explode: Option<bool>,
pub allow_reserved: Option<bool>,
pub inline: Option<bool>,
pub(crate) example: Option<AnyValue>,
}

Expand All @@ -340,14 +328,11 @@ impl ParameterExt {
if from.example.is_some() {
self.example = from.example
}
if from.inline.is_some() {
self.inline = from.inline
}
}

fn parse_once(input: ParseStream) -> syn::Result<Self> {
pub fn parse_once(input: ParseStream) -> syn::Result<Self> {
const EXPECTED_ATTRIBUTE_MESSAGE: &str =
"unexpected attribute, expected any of: style, explode, allow_reserved, example, inline";
"unexpected attribute, expected any of: style, explode, allow_reserved, example";

let ident = input.parse::<Ident>().map_err(|error| {
Error::new(
Expand Down Expand Up @@ -378,10 +363,6 @@ impl ParameterExt {
})?),
..Default::default()
},
"inline" => ParameterExt {
inline: Some(parse_utils::parse_bool_or_true(input)?),
..Default::default()
},
_ => return Err(Error::new(ident.span(), EXPECTED_ATTRIBUTE_MESSAGE)),
};

Expand All @@ -391,6 +372,18 @@ impl ParameterExt {

Ok(ext)
}

pub fn is_parameter_ext(input: ParseStream) -> bool {
let fork = input.fork();
if fork.peek(syn::Ident) {
let ident = fork.parse::<Ident>().unwrap();
let name = &*ident.to_string();

matches!(name, "style" | "explode" | "allow_reserved" | "example")
} else {
false
}
}
}

impl Parse for ParameterExt {
Expand Down
Loading

0 comments on commit 7178d07

Please sign in to comment.