From 0a8d742d8308a7b3fb53ccfd797ee3b3a6cf425c Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Mon, 30 May 2022 14:11:28 +1000 Subject: [PATCH 01/28] Support accepting `params(Type)` for `path` where `Type` implements `IntoParams`, implementing #129 --- examples/todo-warp/src/main.rs | 34 ++++++++++++++++++++-- src/lib.rs | 3 +- utoipa-gen/src/lib.rs | 35 +++++++++++++++++------ utoipa-gen/src/path.rs | 49 +++++++++++++++++++++++++------- utoipa-gen/src/path/parameter.rs | 6 ++-- utoipa-gen/src/schema.rs | 1 - 6 files changed, 99 insertions(+), 29 deletions(-) diff --git a/examples/todo-warp/src/main.rs b/examples/todo-warp/src/main.rs index b0ae63eb..1eec80d1 100644 --- a/examples/todo-warp/src/main.rs +++ b/examples/todo-warp/src/main.rs @@ -94,7 +94,7 @@ mod todo { }; use serde::{Deserialize, Serialize}; - use utoipa::Component; + use utoipa::{openapi::path::ParameterIn, Component, IntoParams}; use warp::{hyper::StatusCode, Filter, Reply}; pub type Store = Arc>>; @@ -110,6 +110,19 @@ mod todo { value: String, } + #[derive(Debug, Deserialize, IntoParams)] + pub struct ListQueryParams { + /// Filters the returned `Todo` items according to whether they contain the specified string. + #[param(style = Form, example = json!("task"))] + contains: Option, + } + + impl utoipa::ParameterIn for ListQueryParams { + fn parameter_in() -> Option { + Some(ParameterIn::Query) + } + } + pub fn handlers() -> impl Filter + Clone { let store = Store::default(); @@ -117,6 +130,7 @@ mod todo { .and(warp::get()) .and(warp::path::end()) .and(with_store(store.clone())) + .and(warp::query::()) .and_then(list_todos); let create = warp::path("todo") @@ -146,14 +160,28 @@ mod todo { #[utoipa::path( get, path = "/todo", + params(ListQueryParams), responses( (status = 200, description = "List todos successfully", body = [Todo]) ) )] - pub async fn list_todos(store: Store) -> Result { + pub async fn list_todos( + store: Store, + query: ListQueryParams, + ) -> Result { let todos = store.lock().unwrap(); - Ok(warp::reply::json(&todos.clone())) + let todos: Vec = if let Some(contains) = query.contains { + todos + .iter() + .filter(|todo| todo.value.contains(&contains)) + .cloned() + .collect() + } else { + todos.clone() + }; + + Ok(warp::reply::json(&todos)) } /// Create new todo item. diff --git a/src/lib.rs b/src/lib.rs index 636c0788..4471bb18 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -472,7 +472,7 @@ pub trait Modify { fn modify(&self, openapi: &mut openapi::OpenApi); } -/// Trait used to convert implementing type to OpenAPI parameters for **actix-web** framework. +/// Trait used to convert implementing type to OpenAPI parameters. /// /// This trait is [derivable][derive] for structs which are used to describe `path` or `query` parameters. /// For more details of `#[derive(IntoParams)]` refer to [derive documentation][derive]. @@ -531,7 +531,6 @@ pub trait Modify { /// } /// ``` /// [derive]: derive.IntoParams.html -#[cfg(feature = "actix_extras")] pub trait IntoParams { /// 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. diff --git a/utoipa-gen/src/lib.rs b/utoipa-gen/src/lib.rs index c5d0137c..7a540a91 100644 --- a/utoipa-gen/src/lib.rs +++ b/utoipa-gen/src/lib.rs @@ -17,7 +17,6 @@ use openapi::OpenApi; use proc_macro::TokenStream; use proc_macro_error::{proc_macro_error, OptionExt, ResultExt}; use quote::{quote, ToTokens, TokenStreamExt}; -#[cfg(feature = "actix_extras")] use schema::into_params::IntoParams; use proc_macro2::{Group, Ident, Punct, TokenStream as TokenStream2}; @@ -436,6 +435,14 @@ pub fn derive_component(input: TokenStream) -> TokenStream { /// /// # Params Attributes /// +/// The `params(...)` attribute can take two forms: [Tuples](#tuples) or [IntoParams +/// Type](#intoparams-type). +/// +/// ## Tuples +/// +/// In the tuples format, paramters are specified using the following attributes inside a list of +/// tuples seperated by commas: +/// /// * `name` _**Must be the first argument**_. Define the name for parameter. /// * `parameter_type` Define possible type for the parameter. Type should be an identifier, slice or option. /// E.g. _`String`_ or _`[String]`_ or _`Option`_. Parameter type is placed after `name` with @@ -450,7 +457,7 @@ pub fn derive_component(input: TokenStream) -> TokenStream { /// * `example = ...` Can be literal value, method reference or _`json!(...)`_. [^json]. Given example /// will override any example in underlying parameter type. /// -/// **Params supports following representation formats:** +/// **For example:** /// /// ```text /// ("id" = String, path, deprecated, description = "Pet database id"), @@ -458,6 +465,17 @@ pub fn derive_component(input: TokenStream) -> TokenStream { /// ("value" = Option<[String]>, query, description = "Value description", style = Form, allow_reserved, deprecated, explode, example = json!(["Value"])) /// ``` /// +/// ## IntoParams Type +/// +/// In the IntoParams paramters format, the paramters are specified using an identifier for a type +/// that implements [`IntoParams`](./trait.IntoParams.html). +/// +/// **For example:** +/// +/// ```text +/// MyParamters +/// ``` +/// /// # Security Requirement Attributes /// /// * `name` Define the name for security requirement. This must match to name of existing @@ -799,7 +817,6 @@ pub fn openapi(input: TokenStream) -> TokenStream { openapi.to_token_stream().into() } -#[cfg(feature = "actix_extras")] #[proc_macro_error] #[proc_macro_derive(IntoParams, attributes(param, into_params))] /// IntoParams derive macro for **actix-web** only. @@ -1263,11 +1280,11 @@ mod parse_utils { next() } - pub fn parse_next_literal_str(input: ParseStream) -> Result { + pub fn parse_next_literal_str(input: ParseStream) -> syn::Result { Ok(parse_next(input, || input.parse::())?.value()) } - pub fn parse_groups(input: ParseStream) -> Result + pub fn parse_groups(input: ParseStream) -> syn::Result where T: Sized, T: Parse, @@ -1277,13 +1294,13 @@ mod parse_utils { groups .into_iter() .map(|group| syn::parse2::(group.stream())) - .collect::>() + .collect::>() }) } pub fn parse_punctuated_within_parenthesis( input: ParseStream, - ) -> Result, Error> + ) -> syn::Result> where T: Parse, { @@ -1292,7 +1309,7 @@ mod parse_utils { Punctuated::::parse_terminated(&content) } - pub fn parse_bool_or_true(input: ParseStream) -> Result { + pub fn parse_bool_or_true(input: ParseStream) -> syn::Result { if input.peek(Token![=]) && input.peek2(LitBool) { input.parse::()?; @@ -1302,7 +1319,7 @@ mod parse_utils { } } - pub fn parse_json_token_stream(input: ParseStream) -> Result { + pub fn parse_json_token_stream(input: ParseStream) -> syn::Result { if input.peek(syn::Ident) && input.peek2(Token![!]) { input.parse::().and_then(|ident| { if ident != "json" { diff --git a/utoipa-gen/src/path.rs b/utoipa-gen/src/path.rs index d2305748..8b5f829f 100644 --- a/utoipa-gen/src/path.rs +++ b/utoipa-gen/src/path.rs @@ -4,6 +4,7 @@ use std::{io::Error, str::FromStr}; use proc_macro2::{Ident, Span, TokenStream as TokenStream2}; use proc_macro_error::abort; use quote::{format_ident, quote, ToTokens}; +use syn::parse::ParseBuffer; use syn::{parenthesized, parse::Parse, Token}; use crate::{component_type::ComponentType, security_requirement::SecurityRequirementAttr, Array}; @@ -64,11 +65,31 @@ pub struct PathAttr<'p> { pub(super) path: Option, operation_id: Option, tag: Option, - params: Option>>, + params: Option>, security: Option>, context_path: Option, } +/// The [`PathAttr::params`] field definition. +enum Params<'p> { + /// A list of tuples of attributes that defines a parameter. + List(Vec>), + /// Identifier for a struct that implements `IntoParams` trait. + Struct(Ident), +} + +impl Parse for Params<'_> { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let params: ParseBuffer; + parenthesized!(params in input); + if params.peek(syn::Ident) { + params.parse::().map(Self::Struct) + } else { + parse_utils::parse_groups(¶ms).map(Self::List) + } + } +} + impl<'p> PathAttr<'p> { #[cfg(any(feature = "actix_extras", feature = "rocket_extras"))] pub fn update_parameters(&mut self, arguments: Option>>) { @@ -185,9 +206,7 @@ impl Parse for PathAttr<'_> { parse_utils::parse_groups::>(&responses)?; } "params" => { - let params; - parenthesized!(params in input); - path_attr.params = Some(parse_utils::parse_groups(¶ms)?); + path_attr.params = Some(input.parse::()?); } "tag" => { path_attr.tag = Some(parse_utils::parse_next_literal_str(input)?); @@ -456,7 +475,7 @@ struct Operation<'a> { summary: Option<&'a String>, description: Option<&'a Vec>, deprecated: &'a Option, - parameters: Option<&'a Vec>>, + parameters: Option<&'a Params<'a>>, request_body: Option<&'a RequestBodyAttr<'a>>, responses: &'a Vec>, security: Option<&'a Array>, @@ -512,12 +531,20 @@ impl ToTokens for Operation<'_> { }) } - if let Some(parameters) = self.parameters { - parameters.iter().for_each(|parameter| match parameter { - Parameter::Value(_) => tokens.extend(quote! { .parameter(#parameter) }), - #[cfg(any(feature = "actix_extras", feature = "rocket_extras"))] - Parameter::TokenStream(_) => tokens.extend(quote! { .parameters(Some(#parameter))}), - }); + match self.parameters { + Some(Params::List(parameters)) => { + parameters.iter().for_each(|parameter| match parameter { + Parameter::Value(_) => tokens.extend(quote! { .parameter(#parameter) }), + #[cfg(any(feature = "actix_extras", feature = "rocket_extras"))] + Parameter::TokenStream(_) => { + tokens.extend(quote! { .parameters(Some(#parameter))}) + } + }); + } + Some(Params::Struct(parameters)) => tokens.extend(quote! { + .parameters(Some(<#parameters as utoipa::IntoParams>::into_params())) + }), + None => {} } } } diff --git a/utoipa-gen/src/path/parameter.rs b/utoipa-gen/src/path/parameter.rs index ee13745a..b9417a56 100644 --- a/utoipa-gen/src/path/parameter.rs +++ b/utoipa-gen/src/path/parameter.rs @@ -155,7 +155,7 @@ impl Parse for Parameter<'_> { impl ToTokens for Parameter<'_> { fn to_tokens(&self, tokens: &mut TokenStream) { - let mut handle_single_parameter = |parameter: &ParameterValue| { + fn handle_single_parameter(tokens: &mut TokenStream, parameter: &ParameterValue) { let name = &*parameter.name; tokens.extend(quote! { utoipa::openapi::path::ParameterBuilder::from(utoipa::openapi::path::Parameter::new(#name)) @@ -191,10 +191,10 @@ impl ToTokens for Parameter<'_> { tokens.extend(quote! { .schema(Some(#property)).required(#required) }); } - }; + } match self { - Parameter::Value(parameter) => handle_single_parameter(parameter), + Parameter::Value(parameter) => handle_single_parameter(tokens, parameter), #[cfg(any(feature = "actix_extras", feature = "rocket_extras"))] Parameter::TokenStream(stream) => { tokens.extend(quote! { #stream }); diff --git a/utoipa-gen/src/schema.rs b/utoipa-gen/src/schema.rs index edc55041..e5f9f744 100644 --- a/utoipa-gen/src/schema.rs +++ b/utoipa-gen/src/schema.rs @@ -7,7 +7,6 @@ use syn::{ use crate::{component_type::ComponentType, Deprecated}; -#[cfg(feature = "actix_extras")] pub mod into_params; pub mod component; From dd6c9a6fcdfdb5eebca75a92eb5f71cfe46804ff Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Mon, 30 May 2022 16:23:17 +1000 Subject: [PATCH 02/28] Add a unit test for IntoParams used with the `path` macro's `params()` field --- Cargo.toml | 1 + tests/path_derive.rs | 90 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index cfbc2686..27fcec85 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ utoipa-gen = { version = "1.1.0", path = "./utoipa-gen" } [dev-dependencies] assert-json-diff = "2" actix-web = { version = "4" } +assert-json-diff = "2" paste = "1" chrono = { version = "0.4", features = ["serde"] } rust_decimal = "1" diff --git a/tests/path_derive.rs b/tests/path_derive.rs index 3480db40..a3e5ce75 100644 --- a/tests/path_derive.rs +++ b/tests/path_derive.rs @@ -1,5 +1,8 @@ #![cfg(feature = "serde_json")] +use assert_json_diff::assert_json_eq; use paste::paste; +use serde_json::{json, Value}; +use utoipa::IntoParams; mod common; @@ -231,6 +234,93 @@ fn derive_path_with_security_requirements() { } } +#[test] +fn derive_path_params_intoparams() { + #[derive(serde::Deserialize, IntoParams)] + struct MyParams { + /// Foo database id. + #[param(style = Form, example = 1)] + #[allow(unused)] + id: u64, + /// Datetime since foo is updated. + #[param(style = Form, example = "2020-04-12T10:23:00Z")] + #[allow(unused)] + since: Option, + } + + impl utoipa::ParameterIn for MyParams { + fn parameter_in() -> Option { + Some(utoipa::openapi::path::ParameterIn::Query) + } + } + + #[utoipa::path( + get, + path = "/list", + responses( + (status = 200, description = "success response") + ), + params(MyParams), + )] + #[allow(unused)] + fn list(params: MyParams) -> String { + "".to_string() + } + + use utoipa::OpenApi; + #[derive(OpenApi, Default)] + #[openapi(handlers(list))] + struct ApiDoc; + + let operation: Value = test_api_fn_doc! { + list, + operation: get, + path: "/list" + }; + + assert_json_eq!( + operation, + json!({ + "deprecated": false, + "description": "", + "operationId": "list", + "parameters": [ + { + "description": "Foo database id.", + "example": 1, + "in": "query", + "name": "id", + "required": true, + "schema": { + "format": "int64", + "type": "integer" + }, + "style": "form" + }, + { + "description": "Datetime since foo is updated.", + "example": "2020-04-12T10:23:00Z", + "in": "query", + "name": "since", + "required": false, + "schema": { + "type": "string" + }, + "style": "form" + } + ], + "responses": { + "200": { + "description": "success response" + } + }, + "tags": [ + "crate" + ] + }) + ) +} + #[cfg(feature = "uuid")] #[test] fn derive_path_with_uuid() { From e7d8fa69dbb240b3ff130c844cfdcb74bf69cc38 Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Mon, 30 May 2022 16:31:37 +1000 Subject: [PATCH 03/28] Reduce scope of unit test --- tests/path_derive.rs | 63 ++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 37 deletions(-) diff --git a/tests/path_derive.rs b/tests/path_derive.rs index a3e5ce75..494dba56 100644 --- a/tests/path_derive.rs +++ b/tests/path_derive.rs @@ -278,46 +278,35 @@ fn derive_path_params_intoparams() { path: "/list" }; + let parameters = operation.get("parameters").unwrap(); + assert_json_eq!( - operation, - json!({ - "deprecated": false, - "description": "", - "operationId": "list", - "parameters": [ - { - "description": "Foo database id.", - "example": 1, - "in": "query", - "name": "id", - "required": true, - "schema": { - "format": "int64", - "type": "integer" - }, - "style": "form" + parameters, + json!([ + { + "description": "Foo database id.", + "example": 1, + "in": "query", + "name": "id", + "required": true, + "schema": { + "format": "int64", + "type": "integer" }, - { - "description": "Datetime since foo is updated.", - "example": "2020-04-12T10:23:00Z", - "in": "query", - "name": "since", - "required": false, - "schema": { - "type": "string" - }, - "style": "form" - } - ], - "responses": { - "200": { - "description": "success response" - } + "style": "form" }, - "tags": [ - "crate" - ] - }) + { + "description": "Datetime since foo is updated.", + "example": "2020-04-12T10:23:00Z", + "in": "query", + "name": "since", + "required": false, + "schema": { + "type": "string" + }, + "style": "form" + } + ]) ) } From a5a4401b2614c4d99033e1c6c62fac4805ae314a Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Tue, 31 May 2022 19:57:59 +1000 Subject: [PATCH 04/28] Implement #151 style container attribute for IntoParams --- tests/path_derive.rs | 11 +-- utoipa-gen/src/lib.rs | 4 +- utoipa-gen/src/path.rs | 2 + utoipa-gen/src/path/parameter.rs | 17 +++- utoipa-gen/src/schema/into_params.rs | 140 +++++++++++++++++++-------- 5 files changed, 119 insertions(+), 55 deletions(-) diff --git a/tests/path_derive.rs b/tests/path_derive.rs index 494dba56..a79e2a3c 100644 --- a/tests/path_derive.rs +++ b/tests/path_derive.rs @@ -237,23 +237,18 @@ fn derive_path_with_security_requirements() { #[test] fn derive_path_params_intoparams() { #[derive(serde::Deserialize, IntoParams)] + #[param(style = Form)] struct MyParams { /// Foo database id. - #[param(style = Form, example = 1)] + #[param(example = 1)] #[allow(unused)] id: u64, /// Datetime since foo is updated. - #[param(style = Form, example = "2020-04-12T10:23:00Z")] + #[param(example = "2020-04-12T10:23:00Z")] #[allow(unused)] since: Option, } - impl utoipa::ParameterIn for MyParams { - fn parameter_in() -> Option { - Some(utoipa::openapi::path::ParameterIn::Query) - } - } - #[utoipa::path( get, path = "/list", diff --git a/utoipa-gen/src/lib.rs b/utoipa-gen/src/lib.rs index 7a540a91..7bd2c281 100644 --- a/utoipa-gen/src/lib.rs +++ b/utoipa-gen/src/lib.rs @@ -915,18 +915,18 @@ pub fn openapi(input: TokenStream) -> TokenStream { /// [^json]: **json** feature need to be enabled for `json!(...)` type to work. pub fn into_params(input: TokenStream) -> TokenStream { let DeriveInput { + attrs, ident, generics, data, - attrs, .. } = syn::parse_macro_input!(input); let into_params = IntoParams { + attrs, generics, data, ident, - attrs, }; into_params.to_token_stream().into() diff --git a/utoipa-gen/src/path.rs b/utoipa-gen/src/path.rs index 8b5f829f..df2c993e 100644 --- a/utoipa-gen/src/path.rs +++ b/utoipa-gen/src/path.rs @@ -71,6 +71,7 @@ pub struct PathAttr<'p> { } /// The [`PathAttr::params`] field definition. +#[cfg_attr(feature = "debug", derive(Debug))] enum Params<'p> { /// A list of tuples of attributes that defines a parameter. List(Vec>), @@ -541,6 +542,7 @@ impl ToTokens for Operation<'_> { } }); } + // TODO: adjust this for the new into_params() signature. Some(Params::Struct(parameters)) => tokens.extend(quote! { .parameters(Some(<#parameters as utoipa::IntoParams>::into_params())) }), diff --git a/utoipa-gen/src/path/parameter.rs b/utoipa-gen/src/path/parameter.rs index b9417a56..2302c124 100644 --- a/utoipa-gen/src/path/parameter.rs +++ b/utoipa-gen/src/path/parameter.rs @@ -9,7 +9,10 @@ use syn::{ #[cfg(any(feature = "actix_extras", feature = "rocket_extras"))] use crate::ext::{Argument, ArgumentIn}; -use crate::{parse_utils, AnyValue, Deprecated, Required, Type}; +use crate::{ + parse_utils, schema::into_params::{IntoParamsAttr, FieldParamContainerAttributes}, AnyValue, Deprecated, Required, + Type, +}; use super::property::Property; @@ -260,8 +263,17 @@ pub struct ParameterExt { pub(crate) example: Option, } +impl From<&'_ FieldParamContainerAttributes<'_>> for ParameterExt { + fn from(attributes: &FieldParamContainerAttributes) -> Self { + Self { + style: attributes.style, + ..ParameterExt::default() + } + } +} + impl ParameterExt { - fn merge(&mut self, from: ParameterExt) { + pub fn merge(&mut self, from: ParameterExt) { if from.style.is_some() { self.style = from.style } @@ -334,6 +346,7 @@ impl Parse for ParameterExt { } /// See definitions from `utoipa` crate path.rs +#[derive(Copy, Clone)] #[cfg_attr(feature = "debug", derive(Debug))] pub enum ParameterStyle { Matrix, diff --git a/utoipa-gen/src/schema/into_params.rs b/utoipa-gen/src/schema/into_params.rs index aac4aff4..f21b9f98 100644 --- a/utoipa-gen/src/schema/into_params.rs +++ b/utoipa-gen/src/schema/into_params.rs @@ -9,47 +9,81 @@ use crate::{ component_type::{ComponentFormat, ComponentType}, doc_comment::CommentAttributes, parse_utils, - path::parameter::ParameterExt, + path::parameter::{ParameterExt, ParameterStyle}, Array, Required, }; use super::{ComponentPart, GenericType, ValueType}; +/// Container attribute `#[into_params(...)]`. +#[derive(Default)] #[cfg_attr(feature = "debug", derive(Debug))] pub struct IntoParamsAttr { + /// See [`ParameterStyle`]. + style: Option, + /// Specify names of unnamed fields with `names(...) attribute.` names: Vec, } +impl IntoParamsAttr { + fn merge(&mut self, other: Self) -> &Self { + if other.style.is_some() { + self.style = other.style; + } + + if !other.names.is_empty() { + self.names = other.names; + } + + self + } +} + impl Parse for IntoParamsAttr { fn parse(input: syn::parse::ParseStream) -> syn::Result { - const EXPECTED_ATTRIBUTE: &str = "unexpected token, expected: names"; - - input - .parse::() - .map_err(|error| Error::new(error.span(), format!("{EXPECTED_ATTRIBUTE}, {error}"))) - .and_then(|ident| { - if ident != "names" { - Err(Error::new(ident.span(), EXPECTED_ATTRIBUTE)) - } else { - Ok(ident) - } + const EXPECTED_ATTRIBUTE: &str = "unexpected token, expected any of: names, style"; + + let mut attributes = Self::default(); + + while !input.is_empty() { + let ident: Ident = input.parse::().map_err(|error| { + Error::new(error.span(), format!("{EXPECTED_ATTRIBUTE}, {error}")) })?; - Ok(IntoParamsAttr { - names: parse_utils::parse_punctuated_within_parenthesis::(input)? - .into_iter() - .map(|name| name.value()) - .collect(), - }) + attributes.merge(match ident.to_string().as_str() { + "names" => Ok(IntoParamsAttr { + names: parse_utils::parse_punctuated_within_parenthesis::(input)? + .into_iter() + .map(|name| name.value()) + .collect(), + ..IntoParamsAttr::default() + }), + "style" => { + let style: ParameterStyle = + parse_utils::parse_next(input, || input.parse::())?; + Ok(IntoParamsAttr { + style: Some(style), + ..IntoParamsAttr::default() + }) + } + _ => Err(Error::new(ident.span(), EXPECTED_ATTRIBUTE)), + }?); + } + + Ok(attributes) } } #[cfg_attr(feature = "debug", derive(Debug))] pub struct IntoParams { + /// Attributes tagged on the whole struct or enum. + pub attrs: Vec, + /// Generics required to complete the definition. pub generics: Generics, + /// Data within the struct or enum. pub data: Data, + /// Name of the struct or enum. pub ident: Ident, - pub attrs: Vec, } impl ToTokens for IntoParams { @@ -71,12 +105,16 @@ impl ToTokens for IntoParams { ) .enumerate() .map(|(index, field)| { - Param( + Param { field, - into_params_attrs - .as_ref() - .and_then(|param| param.names.get(index)), - ) + container_attributes: FieldParamContainerAttributes { + style: into_params_attrs.as_ref().and_then(|attrs| attrs.style), + // TODO: turn this into an error. + name: into_params_attrs + .as_ref() + .map(|attrs| attrs.names.get(index).unwrap()), + }, + } }) .collect::>(); @@ -155,17 +193,31 @@ impl IntoParams { } } -struct Param<'a>(&'a Field, Option<&'a String>); +#[cfg_attr(feature = "debug", derive(Debug))] +pub struct FieldParamContainerAttributes<'a> { + /// See [`IntoParamsAttr::style`]. + pub style: Option, + /// See [`IntoParamsAttr::names`]. The name that applies to this field. + pub name: Option<&'a String>, +} + +#[cfg_attr(feature = "debug", derive(Debug))] +struct Param<'a> { + /// Field in the container used to create a single parameter. + field: &'a Field, + /// Attributes on the container which are relevant for this macro. + container_attributes: FieldParamContainerAttributes<'a>, +} impl ToTokens for Param<'_> { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - let unnamed_field_name = self.1; - let field = self.0; + let field = self.field; let ident = &field.ident; let name = ident .as_ref() .map(|ident| ident.to_string()) - .or_else(|| unnamed_field_name.map(ToString::to_string)) + // TODO: add proper error handling + .or_else(|| self.container_attributes.name.cloned()) .unwrap_or_default(); let component_part = ComponentPart::from_type(&field.ty); let required: Required = @@ -187,25 +239,27 @@ impl ToTokens for Param<'_> { }) } - let parameter_ext = field + let mut parameter_ext = ParameterExt::from(&self.container_attributes); + + // Apply the field attributes if they exist. + field .attrs .iter() .find(|attribute| attribute.path.is_ident("param")) - .map(|attribute| attribute.parse_args::().unwrap_or_abort()); + .map(|attribute| attribute.parse_args::().unwrap_or_abort()) + .map(|p| parameter_ext.merge(p)); - if let Some(ext) = parameter_ext { - if let Some(ref style) = ext.style { - tokens.extend(quote! { .style(Some(#style)) }); - } - if let Some(ref explode) = ext.explode { - tokens.extend(quote! { .explode(Some(#explode)) }); - } - if let Some(ref allow_reserved) = ext.allow_reserved { - tokens.extend(quote! { .allow_reserved(Some(#allow_reserved)) }); - } - if let Some(ref example) = ext.example { - tokens.extend(quote! { .example(Some(#example)) }); - } + if let Some(ref style) = parameter_ext.style { + tokens.extend(quote! { .style(Some(#style)) }); + } + if let Some(ref explode) = parameter_ext.explode { + tokens.extend(quote! { .explode(Some(#explode)) }); + } + if let Some(ref allow_reserved) = parameter_ext.allow_reserved { + tokens.extend(quote! { .allow_reserved(Some(#allow_reserved)) }); + } + if let Some(ref example) = parameter_ext.example { + tokens.extend(quote! { .example(Some(#example)) }); } let param_type = ParamType(&component_part); From 314dd04fe9d1cbea2dcdb055545419e65df53bc5 Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Tue, 31 May 2022 21:46:08 +1000 Subject: [PATCH 05/28] Add parameter_in to the IntoParams container attribute --- tests/path_derive.rs | 2 +- utoipa-gen/src/path/parameter.rs | 17 +++++++++++++- utoipa-gen/src/schema/into_params.rs | 33 ++++++++++++++++++++++++++-- 3 files changed, 48 insertions(+), 4 deletions(-) diff --git a/tests/path_derive.rs b/tests/path_derive.rs index a79e2a3c..e0bafda8 100644 --- a/tests/path_derive.rs +++ b/tests/path_derive.rs @@ -237,7 +237,7 @@ fn derive_path_with_security_requirements() { #[test] fn derive_path_params_intoparams() { #[derive(serde::Deserialize, IntoParams)] - #[param(style = Form)] + #[param(style = Form, parameter_in = query)] struct MyParams { /// Foo database id. #[param(example = 1)] diff --git a/utoipa-gen/src/path/parameter.rs b/utoipa-gen/src/path/parameter.rs index 2302c124..010251d9 100644 --- a/utoipa-gen/src/path/parameter.rs +++ b/utoipa-gen/src/path/parameter.rs @@ -241,6 +241,21 @@ impl FromStr for ParameterIn { } } +impl Parse for ParameterIn { + fn parse(input: ParseStream) -> syn::Result { + const EXPECTED_STYLE: &str = "unexpected in, expected one of: path, query, header, cookie"; + let style = input.parse::()?; + + match &*style.to_string() { + "path" => Ok(Self::Path), + "query" => Ok(Self::Query), + "header" => Ok(Self::Header), + "cookie" => Ok(Self::Cookie), + _ => Err(Error::new(style.span(), EXPECTED_STYLE)), + } + } +} + impl ToTokens for ParameterIn { fn to_tokens(&self, tokens: &mut TokenStream) { tokens.extend(match self { @@ -266,7 +281,7 @@ pub struct ParameterExt { impl From<&'_ FieldParamContainerAttributes<'_>> for ParameterExt { fn from(attributes: &FieldParamContainerAttributes) -> Self { Self { - style: attributes.style, + style: attributes.parameter_style, ..ParameterExt::default() } } diff --git a/utoipa-gen/src/schema/into_params.rs b/utoipa-gen/src/schema/into_params.rs index f21b9f98..e7b501c0 100644 --- a/utoipa-gen/src/schema/into_params.rs +++ b/utoipa-gen/src/schema/into_params.rs @@ -9,7 +9,7 @@ use crate::{ component_type::{ComponentFormat, ComponentType}, doc_comment::CommentAttributes, parse_utils, - path::parameter::{ParameterExt, ParameterStyle}, + path::parameter::{ParameterExt, ParameterIn, ParameterStyle}, Array, Required, }; @@ -23,6 +23,8 @@ pub struct IntoParamsAttr { style: Option, /// Specify names of unnamed fields with `names(...) attribute.` names: Vec, + /// See [`ParameterIn`]. + parameter_in: Option, } impl IntoParamsAttr { @@ -35,13 +37,17 @@ impl IntoParamsAttr { self.names = other.names; } + if other.parameter_in.is_some() { + self.parameter_in = other.parameter_in; + } + self } } impl Parse for IntoParamsAttr { fn parse(input: syn::parse::ParseStream) -> syn::Result { - const EXPECTED_ATTRIBUTE: &str = "unexpected token, expected any of: names, style"; + const EXPECTED_ATTRIBUTE: &str = "unexpected token, expected any of: names, style, parameter_in"; let mut attributes = Self::default(); @@ -66,6 +72,15 @@ impl Parse for IntoParamsAttr { ..IntoParamsAttr::default() }) } + "parameter_in" => { + let parameter_in: ParameterIn = + parse_utils::parse_next(input, || input.parse::())?; + + Ok(IntoParamsAttr { + parameter_in: Some(parameter_in), + ..IntoParamsAttr::default() + }) + } _ => Err(Error::new(ident.span(), EXPECTED_ATTRIBUTE)), }?); } @@ -226,6 +241,20 @@ impl ToTokens for Param<'_> { tokens.extend(quote! { utoipa::openapi::path::ParameterBuilder::new() .name(#name) .parameter_in(parameter_in_provider().unwrap_or_default()) + }); + + // TODO: Remove if no longer required + // if let Some(parameter_in) = &self.container_attributes.parameter_in { + // tokens.extend(quote! { + // .parameter_in(#parameter_in) + // }) + // } else { + // tokens.extend(quote! { + // .parameter_in(::parameter_in().unwrap_or_default()) + // }) + // } + + tokens.extend(quote! { .required(#required) }); From 03622655d2f6aba11a978a63c9680d20771c37ef Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Tue, 31 May 2022 22:12:51 +1000 Subject: [PATCH 06/28] Updated documentation for IntoParams derive macro --- utoipa-gen/src/lib.rs | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/utoipa-gen/src/lib.rs b/utoipa-gen/src/lib.rs index 7bd2c281..d5d445c7 100644 --- a/utoipa-gen/src/lib.rs +++ b/utoipa-gen/src/lib.rs @@ -836,9 +836,23 @@ pub fn openapi(input: TokenStream) -> TokenStream { /// While it is totally okay to declare deprecated with reason /// `#[deprecated = "There is better way to do this"]` the reason would not render in OpenAPI spec. /// -/// # IntoParams Attributes for `#[param(...)]` +/// # IntoParams Container Attributes for `#[param(...)]` /// -/// * `style = ...` Defines how parameters are serialized by [`ParameterStyle`][style]. Default values are based on _`in`_ attribute. +/// The following attributes are available for use in on the container attribute `#[param(...)]` for the struct +/// deriving `IntoParams`: +/// +/// * `style = ...` Defines how all parameters are serialized by [`ParameterStyle`][style]. Default +/// values are based on _`parameter_in`_ attribute. +/// * `parameter_in = ...` = Defines where the parameters of this field are used by +/// [`openapi::path::ParameterIn`][in_enum]. There is no default value, if this attribute is not +/// supplied, then one must implement [`ParameterIn`][in_trait] trait for the struct performing +/// this derive. +/// +/// # IntoParams Field Attributes for `#[param(...)]` +/// +/// 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`_. /// * `allow_reserved` Defines whether reserved characters _`:/?#[]@!$&'()*+,;=`_ is allowed within value. /// * `example = ...` Can be literal value, method reference or _`json!(...)`_. [^json] Given example @@ -905,10 +919,27 @@ pub fn openapi(input: TokenStream) -> TokenStream { /// } /// ``` /// +/// Demonstrate [`IntoParams`][into_params] usage with the `#[param(...)] container attribute:`: +/// ```rust +/// use serde::Deserialize; +/// use utoipa::IntoParams; +/// +/// #[derive(Deserialize, IntoParams)] +/// #[param(style = Form, parameter_in = query)] +/// struct PetPathArgs { +/// /// Id of pet +/// id: i64, +/// /// Name of pet +/// name: String, +/// } +/// ``` +/// /// [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 +/// [in_trait]: utoipa/trait.ParameterIn.html /// /// [^actix]: Feature **actix_extras** need to be enabled /// From e7fe5bca0ca01eb5062b13479cf4ed72233c7a4c Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Tue, 31 May 2022 22:18:20 +1000 Subject: [PATCH 07/28] Updated `param()` macro for IntoParams derive with ParameterIn parsing as PascalCase instead of lowercase, to match the style parsing. --- tests/path_derive.rs | 2 +- utoipa-gen/src/lib.rs | 2 +- utoipa-gen/src/path/parameter.rs | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/path_derive.rs b/tests/path_derive.rs index e0bafda8..fc6cdacc 100644 --- a/tests/path_derive.rs +++ b/tests/path_derive.rs @@ -237,7 +237,7 @@ fn derive_path_with_security_requirements() { #[test] fn derive_path_params_intoparams() { #[derive(serde::Deserialize, IntoParams)] - #[param(style = Form, parameter_in = query)] + #[param(style = Form, parameter_in = Query)] struct MyParams { /// Foo database id. #[param(example = 1)] diff --git a/utoipa-gen/src/lib.rs b/utoipa-gen/src/lib.rs index d5d445c7..85bb3a50 100644 --- a/utoipa-gen/src/lib.rs +++ b/utoipa-gen/src/lib.rs @@ -925,7 +925,7 @@ pub fn openapi(input: TokenStream) -> TokenStream { /// use utoipa::IntoParams; /// /// #[derive(Deserialize, IntoParams)] -/// #[param(style = Form, parameter_in = query)] +/// #[param(style = Form, parameter_in = Query)] /// struct PetPathArgs { /// /// Id of pet /// id: i64, diff --git a/utoipa-gen/src/path/parameter.rs b/utoipa-gen/src/path/parameter.rs index 010251d9..1adff5b6 100644 --- a/utoipa-gen/src/path/parameter.rs +++ b/utoipa-gen/src/path/parameter.rs @@ -243,14 +243,14 @@ impl FromStr for ParameterIn { impl Parse for ParameterIn { fn parse(input: ParseStream) -> syn::Result { - const EXPECTED_STYLE: &str = "unexpected in, expected one of: path, query, header, cookie"; + const EXPECTED_STYLE: &str = "unexpected in, expected one of: Path, Query, Header, Cookie"; let style = input.parse::()?; match &*style.to_string() { - "path" => Ok(Self::Path), - "query" => Ok(Self::Query), - "header" => Ok(Self::Header), - "cookie" => Ok(Self::Cookie), + "Path" => Ok(Self::Path), + "Query" => Ok(Self::Query), + "Header" => Ok(Self::Header), + "Cookie" => Ok(Self::Cookie), _ => Err(Error::new(style.span(), EXPECTED_STYLE)), } } From d0b4755c174a740710027439e55412dbaf761930 Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Tue, 31 May 2022 22:27:21 +1000 Subject: [PATCH 08/28] Updated path and IntoParams documentation --- utoipa-gen/src/lib.rs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/utoipa-gen/src/lib.rs b/utoipa-gen/src/lib.rs index 85bb3a50..a0298e5e 100644 --- a/utoipa-gen/src/lib.rs +++ b/utoipa-gen/src/lib.rs @@ -468,8 +468,10 @@ pub fn derive_component(input: TokenStream) -> TokenStream { /// ## IntoParams Type /// /// In the IntoParams paramters format, the paramters are specified using an identifier for a type -/// that implements [`IntoParams`](./trait.IntoParams.html). +/// that implements [`IntoParams`][into_params]. See [`IntoParams`][into_params] for an +/// example. /// +/// [into_params]: ./trait.IntoParams.html /// **For example:** /// /// ```text @@ -919,18 +921,26 @@ pub fn openapi(input: TokenStream) -> TokenStream { /// } /// ``` /// -/// Demonstrate [`IntoParams`][into_params] usage with the `#[param(...)] container attribute:`: +/// Demonstrate [`IntoParams`][into_params] usage with the `#[param(...)]` container attribute to +/// be used as a path query: /// ```rust /// use serde::Deserialize; /// use utoipa::IntoParams; /// /// #[derive(Deserialize, IntoParams)] /// #[param(style = Form, parameter_in = Query)] -/// struct PetPathArgs { -/// /// Id of pet -/// id: i64, +/// struct PetQuery { /// /// Name of pet -/// name: String, +/// name: Option, +/// /// Age of pet +/// age: Option, +/// } +/// +/// #[utoipa::path( +/// params(PetQuery) +/// )] +/// async get_pet(query: PetQuery) { +/// // ... /// } /// ``` /// From 251634bbf50bb0d3aaf797e6c6e9f53353a3b4f1 Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Thu, 2 Jun 2022 11:25:45 +1000 Subject: [PATCH 09/28] Allow both struct ident and list of parameters to be defined (fixes actix test failure) --- utoipa-gen/src/lib.rs | 15 ++++++++--- utoipa-gen/src/path.rs | 57 ++++++++++++++++++++++++++++-------------- 2 files changed, 49 insertions(+), 23 deletions(-) diff --git a/utoipa-gen/src/lib.rs b/utoipa-gen/src/lib.rs index a0298e5e..71d67ff9 100644 --- a/utoipa-gen/src/lib.rs +++ b/utoipa-gen/src/lib.rs @@ -471,7 +471,7 @@ pub fn derive_component(input: TokenStream) -> TokenStream { /// that implements [`IntoParams`][into_params]. See [`IntoParams`][into_params] for an /// example. /// -/// [into_params]: ./trait.IntoParams.html +/// [into_params]: ./trait.IntoParams.html /// **For example:** /// /// ```text @@ -697,7 +697,9 @@ pub fn path(attr: TokenStream, item: TokenStream) -> TokenStream { let args = resolved_path.as_mut().map(|path| mem::take(&mut path.args)); let arguments = PathOperations::resolve_path_arguments(&ast_fn.sig.inputs, args); - path_attribute.update_parameters(arguments); + path_attribute + .update_parameters(arguments) + .unwrap_or_abort(); } let path = Path::new(path_attribute, fn_name) @@ -937,9 +939,14 @@ pub fn openapi(input: TokenStream) -> TokenStream { /// } /// /// #[utoipa::path( -/// params(PetQuery) +/// get, +/// path = "/get_pet", +/// params(PetQuery), +/// responses( +/// (status = 200, description = "success response") +/// ) /// )] -/// async get_pet(query: PetQuery) { +/// async fn get_pet(query: PetQuery) { /// // ... /// } /// ``` diff --git a/utoipa-gen/src/path.rs b/utoipa-gen/src/path.rs index df2c993e..740d79c1 100644 --- a/utoipa-gen/src/path.rs +++ b/utoipa-gen/src/path.rs @@ -72,11 +72,12 @@ pub struct PathAttr<'p> { /// The [`PathAttr::params`] field definition. #[cfg_attr(feature = "debug", derive(Debug))] -enum Params<'p> { +#[derive(Default)] +struct Params<'p> { /// A list of tuples of attributes that defines a parameter. - List(Vec>), + pub list_params: Vec>, /// Identifier for a struct that implements `IntoParams` trait. - Struct(Ident), + pub struct_ident: Option, } impl Parse for Params<'_> { @@ -84,18 +85,25 @@ impl Parse for Params<'_> { let params: ParseBuffer; parenthesized!(params in input); if params.peek(syn::Ident) { - params.parse::().map(Self::Struct) + params.parse::().map(|struct_ident| Self { + struct_ident: Some(struct_ident), + ..Self::default() + }) } else { - parse_utils::parse_groups(¶ms).map(Self::List) + parse_utils::parse_groups(¶ms).map(|list_params| Self { + list_params, + ..Self::default() + }) } } } impl<'p> PathAttr<'p> { #[cfg(any(feature = "actix_extras", feature = "rocket_extras"))] - pub fn update_parameters(&mut self, arguments: Option>>) { + pub fn update_parameters(&mut self, arguments: Option>>) -> syn::Result<()> { if let Some(arguments) = arguments { if let Some(ref mut parameters) = self.params { + let parameters = &mut parameters.list_params; PathAttr::update_existing_parameters_parameter_types(parameters, &arguments); let new_params = &mut PathAttr::get_new_parameters(parameters, arguments); @@ -107,9 +115,14 @@ impl<'p> PathAttr<'p> { .into_iter() .map(Parameter::from) .for_each(|parameter| params.push(parameter)); - self.params = Some(params); + self.params = Some(Params { + list_params: params, + ..Params::default() + }); } } + + Ok(()) } #[cfg(any(feature = "actix_extras", feature = "rocket_extras"))] @@ -533,19 +546,25 @@ impl ToTokens for Operation<'_> { } match self.parameters { - Some(Params::List(parameters)) => { - parameters.iter().for_each(|parameter| match parameter { - Parameter::Value(_) => tokens.extend(quote! { .parameter(#parameter) }), - #[cfg(any(feature = "actix_extras", feature = "rocket_extras"))] - Parameter::TokenStream(_) => { - tokens.extend(quote! { .parameters(Some(#parameter))}) - } - }); + Some(parameters) => { + parameters + .list_params + .iter() + .for_each(|parameter| match parameter { + Parameter::Value(_) => tokens.extend(quote! { .parameter(#parameter) }), + #[cfg(any(feature = "actix_extras", feature = "rocket_extras"))] + Parameter::TokenStream(_) => { + tokens.extend(quote! { .parameters(Some(#parameter))}) + } + }); + + if let Some(struct_ident) = ¶meters.struct_ident { + // TODO: adjust this for new params signature + tokens.extend(quote! { + .parameters(Some(<#struct_ident as utoipa::IntoParams>::into_params())) + }) + } } - // TODO: adjust this for the new into_params() signature. - Some(Params::Struct(parameters)) => tokens.extend(quote! { - .parameters(Some(<#parameters as utoipa::IntoParams>::into_params())) - }), None => {} } } From 42902cd31a8d470236dbaa91096dce8ebd82798f Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Thu, 2 Jun 2022 11:33:22 +1000 Subject: [PATCH 10/28] Updated documentation for Params --- utoipa-gen/src/path.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/utoipa-gen/src/path.rs b/utoipa-gen/src/path.rs index 740d79c1..c61bbcc5 100644 --- a/utoipa-gen/src/path.rs +++ b/utoipa-gen/src/path.rs @@ -70,7 +70,8 @@ pub struct PathAttr<'p> { context_path: Option, } -/// The [`PathAttr::params`] field definition. +/// The [`PathAttr::params`] field definition. This is parsed from the +/// `#[utoipa::path(params(...))]` attribute. #[cfg_attr(feature = "debug", derive(Debug))] #[derive(Default)] struct Params<'p> { From 8d30f1217c565181e744f941ade8e474e40acd8d Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Thu, 16 Jun 2022 09:48:52 +1000 Subject: [PATCH 11/28] Fixes after rebase from upstream/master --- tests/path_derive.rs | 2 +- utoipa-gen/src/path.rs | 3 +- utoipa-gen/src/path/parameter.rs | 8 ++-- utoipa-gen/src/schema/into_params.rs | 59 ++++++++++++++++++++-------- 4 files changed, 49 insertions(+), 23 deletions(-) diff --git a/tests/path_derive.rs b/tests/path_derive.rs index fc6cdacc..14792bd9 100644 --- a/tests/path_derive.rs +++ b/tests/path_derive.rs @@ -237,7 +237,7 @@ fn derive_path_with_security_requirements() { #[test] fn derive_path_params_intoparams() { #[derive(serde::Deserialize, IntoParams)] - #[param(style = Form, parameter_in = Query)] + #[into_params(style = Form, parameter_in = Query)] struct MyParams { /// Foo database id. #[param(example = 1)] diff --git a/utoipa-gen/src/path.rs b/utoipa-gen/src/path.rs index c61bbcc5..9f8d0f55 100644 --- a/utoipa-gen/src/path.rs +++ b/utoipa-gen/src/path.rs @@ -560,9 +560,8 @@ impl ToTokens for Operation<'_> { }); if let Some(struct_ident) = ¶meters.struct_ident { - // TODO: adjust this for new params signature tokens.extend(quote! { - .parameters(Some(<#struct_ident as utoipa::IntoParams>::into_params())) + .parameters(Some(<#struct_ident as utoipa::IntoParams>::into_params(|| None))) }) } } diff --git a/utoipa-gen/src/path/parameter.rs b/utoipa-gen/src/path/parameter.rs index 1adff5b6..f5a677e4 100644 --- a/utoipa-gen/src/path/parameter.rs +++ b/utoipa-gen/src/path/parameter.rs @@ -10,8 +10,8 @@ use syn::{ #[cfg(any(feature = "actix_extras", feature = "rocket_extras"))] use crate::ext::{Argument, ArgumentIn}; use crate::{ - parse_utils, schema::into_params::{IntoParamsAttr, FieldParamContainerAttributes}, AnyValue, Deprecated, Required, - Type, + parse_utils, schema::into_params::FieldParamContainerAttributes, AnyValue, Deprecated, + Required, Type, }; use super::property::Property; @@ -207,7 +207,7 @@ impl ToTokens for Parameter<'_> { } #[cfg_attr(feature = "debug", derive(Debug))] -#[derive(PartialEq)] +#[derive(PartialEq, Clone, Copy)] pub enum ParameterIn { Query, Path, @@ -281,7 +281,7 @@ pub struct ParameterExt { impl From<&'_ FieldParamContainerAttributes<'_>> for ParameterExt { fn from(attributes: &FieldParamContainerAttributes) -> Self { Self { - style: attributes.parameter_style, + style: attributes.style, ..ParameterExt::default() } } diff --git a/utoipa-gen/src/schema/into_params.rs b/utoipa-gen/src/schema/into_params.rs index e7b501c0..47500ab1 100644 --- a/utoipa-gen/src/schema/into_params.rs +++ b/utoipa-gen/src/schema/into_params.rs @@ -2,7 +2,7 @@ use proc_macro_error::{abort, ResultExt}; use quote::{quote, ToTokens}; use syn::{ parse::Parse, punctuated::Punctuated, token::Comma, Attribute, Data, Error, Field, Generics, - Ident, LitStr, + Ident, LitStr, Token, }; use crate::{ @@ -22,7 +22,7 @@ pub struct IntoParamsAttr { /// See [`ParameterStyle`]. style: Option, /// Specify names of unnamed fields with `names(...) attribute.` - names: Vec, + names: Option>, /// See [`ParameterIn`]. parameter_in: Option, } @@ -33,7 +33,7 @@ impl IntoParamsAttr { self.style = other.style; } - if !other.names.is_empty() { + if !other.names.is_some() { self.names = other.names; } @@ -47,21 +47,30 @@ impl IntoParamsAttr { impl Parse for IntoParamsAttr { fn parse(input: syn::parse::ParseStream) -> syn::Result { - const EXPECTED_ATTRIBUTE: &str = "unexpected token, expected any of: names, style, parameter_in"; + const EXPECTED_ATTRIBUTE: &str = + "unexpected token, expected any of: names, style, parameter_in"; let mut attributes = Self::default(); + let mut first: bool = true; + while !input.is_empty() { + if !first { + input.parse::()?; + } + let ident: Ident = input.parse::().map_err(|error| { Error::new(error.span(), format!("{EXPECTED_ATTRIBUTE}, {error}")) })?; attributes.merge(match ident.to_string().as_str() { "names" => Ok(IntoParamsAttr { - names: parse_utils::parse_punctuated_within_parenthesis::(input)? - .into_iter() - .map(|name| name.value()) - .collect(), + names: Some( + parse_utils::parse_punctuated_within_parenthesis::(input)? + .into_iter() + .map(|name| name.value()) + .collect(), + ), ..IntoParamsAttr::default() }), "style" => { @@ -83,6 +92,8 @@ impl Parse for IntoParamsAttr { } _ => Err(Error::new(ident.span(), EXPECTED_ATTRIBUTE)), }?); + + first = false; } Ok(attributes) @@ -106,7 +117,7 @@ impl ToTokens for IntoParams { let ident = &self.ident; let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl(); - let into_params_attrs = &mut self + let into_params_attrs: Option = self .attrs .iter() .find(|attr| attr.path.is_ident("into_params")) @@ -115,8 +126,8 @@ impl ToTokens for IntoParams { let params = self .get_struct_fields( &into_params_attrs - .as_mut() - .map(|params| params.names.as_ref()), + .as_ref() + .and_then(|params| params.names.as_ref()), ) .enumerate() .map(|(index, field)| { @@ -124,10 +135,15 @@ impl ToTokens for IntoParams { field, container_attributes: FieldParamContainerAttributes { style: into_params_attrs.as_ref().and_then(|attrs| attrs.style), - // TODO: turn this into an error. name: into_params_attrs .as_ref() - .map(|attrs| attrs.names.get(index).unwrap()), + .and_then(|attrs| attrs.names.as_ref()) + .map(|names| names.get(index).unwrap_or_else(|| abort!( + ident, + "There is no name specified in the names(...) attribute for tuple struct field {}", + index + ))), + parameter_in: into_params_attrs.as_ref().and_then(|attrs| attrs.parameter_in), }, } }) @@ -135,11 +151,9 @@ impl ToTokens for IntoParams { tokens.extend(quote! { impl #impl_generics utoipa::IntoParams for #ident #ty_generics #where_clause { - fn into_params(parameter_in_provider: impl Fn() -> Option) -> Vec { #params.to_vec() } - } }); } @@ -214,6 +228,8 @@ pub struct FieldParamContainerAttributes<'a> { pub style: Option, /// See [`IntoParamsAttr::names`]. The name that applies to this field. pub name: Option<&'a String>, + /// See [`IntoParamsAttr::parameter_in`]. + pub parameter_in: Option, } #[cfg_attr(feature = "debug", derive(Debug))] @@ -240,9 +256,20 @@ impl ToTokens for Param<'_> { tokens.extend(quote! { utoipa::openapi::path::ParameterBuilder::new() .name(#name) - .parameter_in(parameter_in_provider().unwrap_or_default()) }); + tokens.extend( + if let Some(parameter_in) = self.container_attributes.parameter_in { + quote! { + .parameter_in(#parameter_in) + } + } else { + quote! { + .parameter_in(parameter_in_provider().unwrap_or_default()) + } + }, + ); + // TODO: Remove if no longer required // if let Some(parameter_in) = &self.container_attributes.parameter_in { // tokens.extend(quote! { From 1738cee23ede6a8b60665f175ac9597cba6a500c Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Thu, 16 Jun 2022 09:58:18 +1000 Subject: [PATCH 12/28] Fix failing doctests --- src/lib.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 4471bb18..4a05ad25 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -482,7 +482,7 @@ pub trait Modify { /// 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)] @@ -503,12 +503,14 @@ pub trait Modify { /// # name: String, /// # } /// impl utoipa::IntoParams for PetParams { -/// fn into_params() -> Vec { +/// fn into_params( +/// parameter_in_provider: impl Fn() -> Option +/// ) -> Vec { /// vec![ /// utoipa::openapi::path::ParameterBuilder::new() /// .name("id") /// .required(utoipa::openapi::Required::True) -/// .parameter_in(utoipa::openapi::path::ParameterIn::Path) +/// .parameter_in(parameter_in_provider().unwrap_or_default()) /// .description(Some("Id of pet")) /// .schema(Some( /// utoipa::openapi::PropertyBuilder::new() @@ -519,7 +521,7 @@ pub trait Modify { /// utoipa::openapi::path::ParameterBuilder::new() /// .name("name") /// .required(utoipa::openapi::Required::True) -/// .parameter_in(utoipa::openapi::path::ParameterIn::Path) +/// .parameter_in(parameter_in_provider().unwrap_or_default()) /// .description(Some("Name of pet")) /// .schema(Some( /// utoipa::openapi::PropertyBuilder::new() From df5f5ae5844be9dee5eb88e8036c45a8592abae4 Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Thu, 16 Jun 2022 10:01:03 +1000 Subject: [PATCH 13/28] Remove duplicate Cargo.toml key asser-json-diff --- Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 27fcec85..0fca0487 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,7 +38,6 @@ serde_yaml = { version = "0.8", optional = true } utoipa-gen = { version = "1.1.0", path = "./utoipa-gen" } [dev-dependencies] -assert-json-diff = "2" actix-web = { version = "4" } assert-json-diff = "2" paste = "1" From 975c8c9d483f528e01416993442e1bdfa26e8d3d Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Thu, 16 Jun 2022 10:15:06 +1000 Subject: [PATCH 14/28] Remove todos and improve error handling --- utoipa-gen/src/schema/into_params.rs | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/utoipa-gen/src/schema/into_params.rs b/utoipa-gen/src/schema/into_params.rs index 47500ab1..c0042666 100644 --- a/utoipa-gen/src/schema/into_params.rs +++ b/utoipa-gen/src/schema/into_params.rs @@ -140,7 +140,7 @@ impl ToTokens for IntoParams { .and_then(|attrs| attrs.names.as_ref()) .map(|names| names.get(index).unwrap_or_else(|| abort!( ident, - "There is no name specified in the names(...) attribute for tuple struct field {}", + "There is no name specified in the names(...) container attribute for tuple struct field {}", index ))), parameter_in: into_params_attrs.as_ref().and_then(|attrs| attrs.parameter_in), @@ -247,9 +247,11 @@ impl ToTokens for Param<'_> { let name = ident .as_ref() .map(|ident| ident.to_string()) - // TODO: add proper error handling .or_else(|| self.container_attributes.name.cloned()) - .unwrap_or_default(); + .unwrap_or_else(|| abort!( + field, "No name specified for unnamed field."; + help = "Try adding #[into_params(names(...))] container attribute to specify the name for this field" + )); let component_part = ComponentPart::from_type(&field.ty); let required: Required = (!matches!(&component_part.generic_type, Some(GenericType::Option))).into(); @@ -270,17 +272,6 @@ impl ToTokens for Param<'_> { }, ); - // TODO: Remove if no longer required - // if let Some(parameter_in) = &self.container_attributes.parameter_in { - // tokens.extend(quote! { - // .parameter_in(#parameter_in) - // }) - // } else { - // tokens.extend(quote! { - // .parameter_in(::parameter_in().unwrap_or_default()) - // }) - // } - tokens.extend(quote! { .required(#required) }); From 6f989e09eda8a3cfd6bd19df65035e37fd8e9ffc Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Thu, 16 Jun 2022 12:35:43 +1000 Subject: [PATCH 15/28] Fix bug with names not being parsed from container attribute --- utoipa-gen/src/schema/into_params.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/utoipa-gen/src/schema/into_params.rs b/utoipa-gen/src/schema/into_params.rs index c0042666..25bcd189 100644 --- a/utoipa-gen/src/schema/into_params.rs +++ b/utoipa-gen/src/schema/into_params.rs @@ -33,7 +33,7 @@ impl IntoParamsAttr { self.style = other.style; } - if !other.names.is_some() { + if other.names.is_some() { self.names = other.names; } @@ -214,7 +214,7 @@ impl IntoParams { None => { abort! { ident, - "struct with unnamed fields must have explisit name declarations."; + "struct with unnamed fields must have explicit name declarations."; help = "Try defining `#[into_params(names(...))]` over your type: {}", ident, } } From 3a34e3868709f341710f3f72f5d8bb91e30cb564 Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Thu, 16 Jun 2022 16:28:42 +1000 Subject: [PATCH 16/28] Allow params to be specified using both struct and bracket style --- tests/path_derive.rs | 22 ++++- utoipa-gen/src/path.rs | 59 +++++------- utoipa-gen/src/path/parameter.rs | 151 +++++++++++++++++-------------- 3 files changed, 124 insertions(+), 108 deletions(-) diff --git a/tests/path_derive.rs b/tests/path_derive.rs index 14792bd9..e85408b7 100644 --- a/tests/path_derive.rs +++ b/tests/path_derive.rs @@ -251,14 +251,17 @@ fn derive_path_params_intoparams() { #[utoipa::path( get, - path = "/list", + path = "/list/{id}", responses( (status = 200, description = "success response") ), - params(MyParams), + params( + MyParams, + ("id" = i64, path, description = "Id of some items to list") + ) )] #[allow(unused)] - fn list(params: MyParams) -> String { + fn list(id: i64, params: MyParams) -> String { "".to_string() } @@ -270,7 +273,7 @@ fn derive_path_params_intoparams() { let operation: Value = test_api_fn_doc! { list, operation: get, - path: "/list" + path: "/list/{id}" }; let parameters = operation.get("parameters").unwrap(); @@ -300,6 +303,17 @@ fn derive_path_params_intoparams() { "type": "string" }, "style": "form" + }, + { + "deprecated": false, + "description": "Id of some items to list", + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "int64", + "type": "integer" + } } ]) ) diff --git a/utoipa-gen/src/path.rs b/utoipa-gen/src/path.rs index 9f8d0f55..09941d68 100644 --- a/utoipa-gen/src/path.rs +++ b/utoipa-gen/src/path.rs @@ -4,7 +4,9 @@ use std::{io::Error, str::FromStr}; use proc_macro2::{Ident, Span, TokenStream as TokenStream2}; use proc_macro_error::abort; use quote::{format_ident, quote, ToTokens}; +use syn::buffer::TokenBuffer; use syn::parse::ParseBuffer; +use syn::punctuated::Punctuated; use syn::{parenthesized, parse::Parse, Token}; use crate::{component_type::ComponentType, security_requirement::SecurityRequirementAttr, Array}; @@ -76,26 +78,23 @@ pub struct PathAttr<'p> { #[derive(Default)] struct Params<'p> { /// A list of tuples of attributes that defines a parameter. - pub list_params: Vec>, - /// Identifier for a struct that implements `IntoParams` trait. - pub struct_ident: Option, + pub parameters: Vec>, } impl Parse for Params<'_> { fn parse(input: syn::parse::ParseStream) -> syn::Result { - let params: ParseBuffer; - parenthesized!(params in input); - if params.peek(syn::Ident) { - params.parse::().map(|struct_ident| Self { - struct_ident: Some(struct_ident), - ..Self::default() - }) - } else { - parse_utils::parse_groups(¶ms).map(|list_params| Self { - list_params, - ..Self::default() - }) - } + let params: Vec = Punctuated::::parse_terminated(input) + .map(|punctuated| punctuated.into_iter().collect::>())?; + + Ok(Self { parameters: params }) + } +} + +impl ToTokens for Params<'_> { + fn to_tokens(&self, tokens: &mut TokenStream2) { + self.parameters + .iter() + .for_each(|parameter| parameter.to_tokens(tokens)); } } @@ -221,7 +220,9 @@ impl Parse for PathAttr<'_> { parse_utils::parse_groups::>(&responses)?; } "params" => { - path_attr.params = Some(input.parse::()?); + let params; + parenthesized!(params in input); + path_attr.params = Some(params.parse()?); } "tag" => { path_attr.tag = Some(parse_utils::parse_next_literal_str(input)?); @@ -485,6 +486,7 @@ impl ToTokens for Path<'_> { } } +#[cfg_attr(feature = "debug", derive(Debug))] struct Operation<'a> { operation_id: &'a String, summary: Option<&'a String>, @@ -546,27 +548,8 @@ impl ToTokens for Operation<'_> { }) } - match self.parameters { - Some(parameters) => { - parameters - .list_params - .iter() - .for_each(|parameter| match parameter { - Parameter::Value(_) => tokens.extend(quote! { .parameter(#parameter) }), - #[cfg(any(feature = "actix_extras", feature = "rocket_extras"))] - Parameter::TokenStream(_) => { - tokens.extend(quote! { .parameters(Some(#parameter))}) - } - }); - - if let Some(struct_ident) = ¶meters.struct_ident { - tokens.extend(quote! { - .parameters(Some(<#struct_ident as utoipa::IntoParams>::into_params(|| None))) - }) - } - } - None => {} - } + self.parameters + .map(|parameters| parameters.to_tokens(tokens)); } } diff --git a/utoipa-gen/src/path/parameter.rs b/utoipa-gen/src/path/parameter.rs index f5a677e4..18e77827 100644 --- a/utoipa-gen/src/path/parameter.rs +++ b/utoipa-gen/src/path/parameter.rs @@ -3,7 +3,8 @@ use std::{borrow::Cow, str::FromStr}; use proc_macro2::{Ident, Span, TokenStream}; use quote::{quote, ToTokens}; use syn::{ - parse::{Parse, ParseStream}, + parenthesized, + parse::{Parse, ParseBuffer, ParseStream}, Error, LitStr, Token, }; @@ -30,10 +31,35 @@ use super::property::Property; #[cfg_attr(feature = "debug", derive(Debug))] pub enum Parameter<'a> { Value(ParameterValue<'a>), + /// Identifier for a struct that implements `IntoParams` trait. + Struct(Ident), #[cfg(any(feature = "actix_extras", feature = "rocket_extras"))] TokenStream(TokenStream), } +impl Parse for Parameter<'_> { + fn parse(input: ParseStream) -> syn::Result { + if input.peek(syn::Ident) { + Ok(Self::Struct(input.parse()?)) + } else { + Ok(Self::Value(input.parse()?)) + } + } +} + +impl ToTokens for Parameter<'_> { + fn to_tokens(&self, tokens: &mut TokenStream) { + match self { + Parameter::Value(parameter) => tokens.extend(quote! { .parameter(#parameter) }), + Parameter::Struct(struct_ident) => tokens.extend(quote! { + .parameters(Some(<#struct_ident as utoipa::IntoParams>::into_params(|| None))) + }), + #[cfg(any(feature = "actix_extras", feature = "rocket_extras"))] + Parameter::TokenStream(stream) => tokens.extend(quote! { .parameters(Some(#stream))}), + } + } +} + #[derive(Default)] #[cfg_attr(feature = "debug", derive(Debug))] pub struct ParameterValue<'a> { @@ -57,29 +83,11 @@ impl<'p> ParameterValue<'p> { } } -#[cfg(any(feature = "actix_extras", feature = "rocket_extras"))] -impl<'a> From> for Parameter<'a> { - fn from(argument: Argument<'a>) -> Self { - match argument { - Argument::Value(value) => Self::Value(ParameterValue { - name: value.name.unwrap_or_else(|| Cow::Owned(String::new())), - parameter_in: if value.argument_in == ArgumentIn::Path { - ParameterIn::Path - } else { - ParameterIn::Query - }, - parameter_type: value - .ident - .map(|ty| Type::new(Cow::Borrowed(ty), value.is_array, value.is_option)), - ..Default::default() - }), - Argument::TokenStream(stream) => Self::TokenStream(stream), - } - } -} +impl Parse for ParameterValue<'_> { + fn parse(input_with_parens: ParseStream) -> syn::Result { + let input: ParseBuffer; + parenthesized!(input in input_with_parens); -impl Parse for Parameter<'_> { - fn parse(input: ParseStream) -> syn::Result { let mut parameter = ParameterValue::default(); if input.peek(LitStr) { @@ -88,7 +96,7 @@ impl Parse for Parameter<'_> { parameter.name = Cow::Owned(name); if input.peek(Token![=]) { - parameter.parameter_type = Some(parse_utils::parse_next(input, || { + parameter.parameter_type = Some(parse_utils::parse_next(&input, || { input.parse().map_err(|error| { Error::new( error.span(), @@ -138,10 +146,10 @@ impl Parse for Parameter<'_> { .parse::() .map_err(|error| Error::new(ident.span(), error))?; } - "deprecated" => parameter.deprecated = parse_utils::parse_bool_or_true(input)?, + "deprecated" => parameter.deprecated = parse_utils::parse_bool_or_true(&input)?, "description" => { parameter.description = Some( - parse_utils::parse_next(input, || input.parse::())?.value(), + parse_utils::parse_next(&input, || input.parse::())?.value(), ) } _ => return Err(Error::new(ident.span(), EXPECTED_ATTRIBUTE_MESSAGE)), @@ -152,56 +160,67 @@ impl Parse for Parameter<'_> { } } - Ok(Parameter::Value(parameter)) + Ok(parameter) } } -impl ToTokens for Parameter<'_> { +impl ToTokens for ParameterValue<'_> { fn to_tokens(&self, tokens: &mut TokenStream) { - fn handle_single_parameter(tokens: &mut TokenStream, parameter: &ParameterValue) { - let name = &*parameter.name; - tokens.extend(quote! { - utoipa::openapi::path::ParameterBuilder::from(utoipa::openapi::path::Parameter::new(#name)) - }); - let parameter_in = ¶meter.parameter_in; - tokens.extend(quote! { .parameter_in(#parameter_in) }); - - let deprecated: Deprecated = parameter.deprecated.into(); - tokens.extend(quote! { .deprecated(Some(#deprecated)) }); - - if let Some(ref description) = parameter.description { - tokens.extend(quote! { .description(Some(#description)) }); - } + let name = &*self.name; + tokens.extend(quote! { + utoipa::openapi::path::ParameterBuilder::from(utoipa::openapi::path::Parameter::new(#name)) + }); + let parameter_in = &self.parameter_in; + tokens.extend(quote! { .parameter_in(#parameter_in) }); + + let deprecated: Deprecated = self.deprecated.into(); + tokens.extend(quote! { .deprecated(Some(#deprecated)) }); + + if let Some(ref description) = self.description { + tokens.extend(quote! { .description(Some(#description)) }); + } - if let Some(ref ext) = parameter.parameter_ext { - if let Some(ref style) = ext.style { - tokens.extend(quote! { .style(Some(#style)) }); - } - if let Some(ref explode) = ext.explode { - tokens.extend(quote! { .explode(Some(#explode)) }); - } - if let Some(ref allow_reserved) = ext.allow_reserved { - tokens.extend(quote! { .allow_reserved(Some(#allow_reserved)) }); - } - if let Some(ref example) = ext.example { - tokens.extend(quote! { .example(Some(#example)) }); - } + if let Some(ref ext) = self.parameter_ext { + if let Some(ref style) = ext.style { + tokens.extend(quote! { .style(Some(#style)) }); } + if let Some(ref explode) = ext.explode { + tokens.extend(quote! { .explode(Some(#explode)) }); + } + if let Some(ref allow_reserved) = ext.allow_reserved { + tokens.extend(quote! { .allow_reserved(Some(#allow_reserved)) }); + } + if let Some(ref example) = ext.example { + tokens.extend(quote! { .example(Some(#example)) }); + } + } - if let Some(ref parameter_type) = parameter.parameter_type { - let property = Property::new(parameter_type.is_array, ¶meter_type.ty); - let required: Required = (!parameter_type.is_option).into(); + if let Some(ref parameter_type) = self.parameter_type { + let property = Property::new(parameter_type.is_array, ¶meter_type.ty); + let required: Required = (!parameter_type.is_option).into(); - tokens.extend(quote! { .schema(Some(#property)).required(#required) }); - } + tokens.extend(quote! { .schema(Some(#property)).required(#required) }); } + } +} - match self { - Parameter::Value(parameter) => handle_single_parameter(tokens, parameter), - #[cfg(any(feature = "actix_extras", feature = "rocket_extras"))] - Parameter::TokenStream(stream) => { - tokens.extend(quote! { #stream }); - } +#[cfg(any(feature = "actix_extras", feature = "rocket_extras"))] +impl<'a> From> for Parameter<'a> { + fn from(argument: Argument<'a>) -> Self { + match argument { + Argument::Value(value) => Self::Value(ParameterValue { + name: value.name.unwrap_or_else(|| Cow::Owned(String::new())), + parameter_in: if value.argument_in == ArgumentIn::Path { + ParameterIn::Path + } else { + ParameterIn::Query + }, + parameter_type: value + .ident + .map(|ty| Type::new(Cow::Borrowed(ty), value.is_array, value.is_option)), + ..Default::default() + }), + Argument::TokenStream(stream) => Self::TokenStream(stream), } } } From 13c7912e84bc42828133436756371d1b906b7ad1 Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Thu, 16 Jun 2022 16:42:02 +1000 Subject: [PATCH 17/28] Updated path macro documentation --- utoipa-gen/src/lib.rs | 49 +++++++++++++++++++++++++++++++++--------- utoipa-gen/src/path.rs | 2 -- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/utoipa-gen/src/lib.rs b/utoipa-gen/src/lib.rs index 71d67ff9..45a1c2c6 100644 --- a/utoipa-gen/src/lib.rs +++ b/utoipa-gen/src/lib.rs @@ -403,20 +403,27 @@ pub fn derive_component(input: TokenStream) -> TokenStream { /// /// **Minimal response format:** /// ```text -/// (status = 200, description = "success response") +/// responses( +/// (status = 200, description = "success response"), +/// (status = 404, description = "resource missing"), +/// ) /// ``` /// /// **Response with all possible values:** /// ```text -/// (status = 200, description = "Success response", body = Pet, content_type = "application/json", -/// headers(...), -/// example = json!({"id": 1, "name": "bob the cat"}) +/// responses( +/// (status = 200, description = "Success response", body = Pet, content_type = "application/json", +/// headers(...), +/// example = json!({"id": 1, "name": "bob the cat"}) +/// ) /// ) /// ``` /// /// **Response with multiple response content types:** /// ```text -/// (status = 200, description = "Success response", body = Pet, content_type = ["application/json", "text/xml"]) +/// responses( +/// (status = 200, description = "Success response", body = Pet, content_type = ["application/json", "text/xml"]) +/// ) /// ``` /// /// # Response Header Attributes @@ -435,7 +442,7 @@ pub fn derive_component(input: TokenStream) -> TokenStream { /// /// # Params Attributes /// -/// The `params(...)` attribute can take two forms: [Tuples](#tuples) or [IntoParams +/// The list of attributes inside the `params(...)` attribute can take two forms: [Tuples](#tuples) or [IntoParams /// Type](#intoparams-type). /// /// ## Tuples @@ -460,9 +467,20 @@ pub fn derive_component(input: TokenStream) -> TokenStream { /// **For example:** /// /// ```text -/// ("id" = String, path, deprecated, description = "Pet database id"), -/// ("id", path, deprecated, description = "Pet database id"), -/// ("value" = Option<[String]>, query, description = "Value description", style = Form, allow_reserved, deprecated, explode, example = json!(["Value"])) +/// params( +/// ("id" = String, path, deprecated, description = "Pet database id"), +/// ("name", path, deprecated, description = "Pet name"), +/// ( +/// "value" = Option<[String]>, +/// query, +/// description = "Value description", +/// style = Form, +/// allow_reserved, +/// deprecated, +/// explode, +/// example = json!(["Value"])) +/// ) +/// ) /// ``` /// /// ## IntoParams Type @@ -475,7 +493,18 @@ pub fn derive_component(input: TokenStream) -> TokenStream { /// **For example:** /// /// ```text -/// MyParamters +/// params(MyParamters) +/// ``` +/// +/// Note that `MyParameters` can also be used in combination with the [tuples +/// representation](#tuples) or other structs. **For example:** +/// +/// ```text +/// params( +/// MyParameters1, +/// MyParameters2, +/// ("id" = String, path, deprecated, description = "Pet database id"), +/// ) /// ``` /// /// # Security Requirement Attributes diff --git a/utoipa-gen/src/path.rs b/utoipa-gen/src/path.rs index 09941d68..2a1eeb46 100644 --- a/utoipa-gen/src/path.rs +++ b/utoipa-gen/src/path.rs @@ -4,8 +4,6 @@ use std::{io::Error, str::FromStr}; use proc_macro2::{Ident, Span, TokenStream as TokenStream2}; use proc_macro_error::abort; use quote::{format_ident, quote, ToTokens}; -use syn::buffer::TokenBuffer; -use syn::parse::ParseBuffer; use syn::punctuated::Punctuated; use syn::{parenthesized, parse::Parse, Token}; From b0b4e422f6c689071f35faee1b365fa7805af184 Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Thu, 16 Jun 2022 17:01:25 +1000 Subject: [PATCH 18/28] Make the struct param use ExprPath instead of Ident to support paths to structs --- utoipa-gen/src/path/parameter.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/utoipa-gen/src/path/parameter.rs b/utoipa-gen/src/path/parameter.rs index 18e77827..c15f8e16 100644 --- a/utoipa-gen/src/path/parameter.rs +++ b/utoipa-gen/src/path/parameter.rs @@ -5,7 +5,7 @@ use quote::{quote, ToTokens}; use syn::{ parenthesized, parse::{Parse, ParseBuffer, ParseStream}, - Error, LitStr, Token, + Error, LitStr, Token, ExprPath, }; #[cfg(any(feature = "actix_extras", feature = "rocket_extras"))] @@ -32,7 +32,7 @@ use super::property::Property; pub enum Parameter<'a> { Value(ParameterValue<'a>), /// Identifier for a struct that implements `IntoParams` trait. - Struct(Ident), + Struct(ExprPath), #[cfg(any(feature = "actix_extras", feature = "rocket_extras"))] TokenStream(TokenStream), } From 9033a64b072235c1d353d004baf0ad6bf7ec40bc Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Thu, 16 Jun 2022 17:14:45 +1000 Subject: [PATCH 19/28] Fix failing tests for utoipa-gen --- utoipa-gen/src/path.rs | 43 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/utoipa-gen/src/path.rs b/utoipa-gen/src/path.rs index 2a1eeb46..b762be60 100644 --- a/utoipa-gen/src/path.rs +++ b/utoipa-gen/src/path.rs @@ -101,20 +101,20 @@ impl<'p> PathAttr<'p> { pub fn update_parameters(&mut self, arguments: Option>>) -> syn::Result<()> { if let Some(arguments) = arguments { if let Some(ref mut parameters) = self.params { - let parameters = &mut parameters.list_params; + let parameters = &mut parameters.parameters; PathAttr::update_existing_parameters_parameter_types(parameters, &arguments); let new_params = &mut PathAttr::get_new_parameters(parameters, arguments); parameters.append(new_params); } else { // no parameters at all, add arguments to the parameters - let mut params = Vec::with_capacity(arguments.len()); + let mut parameters = Vec::with_capacity(arguments.len()); arguments .into_iter() .map(Parameter::from) - .for_each(|parameter| params.push(parameter)); + .for_each(|parameter| parameters.push(parameter)); self.params = Some(Params { - list_params: params, + parameters, ..Params::default() }); } @@ -131,25 +131,24 @@ impl<'p> PathAttr<'p> { use std::borrow::Cow; parameters .iter_mut() - .filter_map(|parameter| match parameter { - Parameter::Value(value) => Some(value), - Parameter::TokenStream(_) => None, - }) - .for_each(|parameter| { - if let Some(argument) = arguments.iter().find_map(|argument| match argument { - Argument::Value(value) - if value.name.as_ref() == Some(&*Cow::Borrowed(¶meter.name)) => - { - Some(value) + .for_each(|parameter: &mut Parameter<'a>| match parameter { + Parameter::Value(parameter) => { + if let Some(argument) = arguments.iter().find_map(|argument| match argument { + Argument::Value(value) + if value.name.as_ref() == Some(&*Cow::Borrowed(¶meter.name)) => + { + Some(value) + } + _ => None, + }) { + parameter.update_parameter_type( + argument.ident, + argument.is_array, + argument.is_option, + ) } - _ => None, - }) { - parameter.update_parameter_type( - argument.ident, - argument.is_array, - argument.is_option, - ) - } + }, + Parameter::Struct(_) | Parameter::TokenStream(_) => {}, }); } From cc099bfbafd7c60c9d6780ca7eea544a6c8d7ff2 Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Thu, 16 Jun 2022 19:40:05 +1000 Subject: [PATCH 20/28] Fix compile error for todo-warp project --- examples/todo-warp/src/main.rs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/examples/todo-warp/src/main.rs b/examples/todo-warp/src/main.rs index 1eec80d1..ab6768da 100644 --- a/examples/todo-warp/src/main.rs +++ b/examples/todo-warp/src/main.rs @@ -63,7 +63,9 @@ async fn serve_swagger( config: Arc>, ) -> Result, Rejection> { if full_path.as_str() == "/swagger-ui" { - return Ok(Box::new(warp::redirect::found(Uri::from_static("/swagger-ui/")))); + return Ok(Box::new(warp::redirect::found(Uri::from_static( + "/swagger-ui/", + )))); } let path = tail.as_str(); @@ -94,7 +96,7 @@ mod todo { }; use serde::{Deserialize, Serialize}; - use utoipa::{openapi::path::ParameterIn, Component, IntoParams}; + use utoipa::{Component, IntoParams}; use warp::{hyper::StatusCode, Filter, Reply}; pub type Store = Arc>>; @@ -111,18 +113,13 @@ mod todo { } #[derive(Debug, Deserialize, IntoParams)] + #[into_params(parameter_in = Query)] pub struct ListQueryParams { /// Filters the returned `Todo` items according to whether they contain the specified string. #[param(style = Form, example = json!("task"))] contains: Option, } - impl utoipa::ParameterIn for ListQueryParams { - fn parameter_in() -> Option { - Some(ParameterIn::Query) - } - } - pub fn handlers() -> impl Filter + Clone { let store = Store::default(); From 9cb66d926f9360bd1dc6240b277eade1a0f56f15 Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Fri, 17 Jun 2022 15:10:55 +1000 Subject: [PATCH 21/28] Fix typo in docs in utoipa-gen/src/lib.rs Co-authored-by: Juha Kukkonen --- utoipa-gen/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utoipa-gen/src/lib.rs b/utoipa-gen/src/lib.rs index 45a1c2c6..f029de53 100644 --- a/utoipa-gen/src/lib.rs +++ b/utoipa-gen/src/lib.rs @@ -485,7 +485,7 @@ pub fn derive_component(input: TokenStream) -> TokenStream { /// /// ## IntoParams Type /// -/// In the IntoParams paramters format, the paramters are specified using an identifier for a type +/// In the IntoParams parameters format, the parameters are specified using an identifier for a type /// that implements [`IntoParams`][into_params]. See [`IntoParams`][into_params] for an /// example. /// From 0635e9f97f0dd6348ef4740a95896cb0010f5229 Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Fri, 17 Jun 2022 15:11:22 +1000 Subject: [PATCH 22/28] Fix typo in docs in utoipa-gen/src/lib.rs Co-authored-by: Juha Kukkonen --- utoipa-gen/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utoipa-gen/src/lib.rs b/utoipa-gen/src/lib.rs index f029de53..28c55c76 100644 --- a/utoipa-gen/src/lib.rs +++ b/utoipa-gen/src/lib.rs @@ -447,7 +447,7 @@ pub fn derive_component(input: TokenStream) -> TokenStream { /// /// ## Tuples /// -/// In the tuples format, paramters are specified using the following attributes inside a list of +/// In the tuples format, parameters are specified using the following attributes inside a list of /// tuples seperated by commas: /// /// * `name` _**Must be the first argument**_. Define the name for parameter. From e1e6b40a170a56c37f2671cd9a90cbb753e669e4 Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Fri, 17 Jun 2022 15:11:36 +1000 Subject: [PATCH 23/28] Fix typo in docs in utoipa-gen/src/lib.rs Co-authored-by: Juha Kukkonen --- utoipa-gen/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utoipa-gen/src/lib.rs b/utoipa-gen/src/lib.rs index 28c55c76..8edd4ea9 100644 --- a/utoipa-gen/src/lib.rs +++ b/utoipa-gen/src/lib.rs @@ -493,7 +493,7 @@ pub fn derive_component(input: TokenStream) -> TokenStream { /// **For example:** /// /// ```text -/// params(MyParamters) +/// params(MyParameters) /// ``` /// /// Note that `MyParameters` can also be used in combination with the [tuples From 8b9b9c109b599c37a639b514e1901e1a588728d3 Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Fri, 17 Jun 2022 15:21:36 +1000 Subject: [PATCH 24/28] Remove unused Result from path.rs --- utoipa-gen/src/lib.rs | 4 +--- utoipa-gen/src/path.rs | 8 +++----- utoipa-gen/src/path/parameter.rs | 2 +- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/utoipa-gen/src/lib.rs b/utoipa-gen/src/lib.rs index 8edd4ea9..ded717e2 100644 --- a/utoipa-gen/src/lib.rs +++ b/utoipa-gen/src/lib.rs @@ -726,9 +726,7 @@ pub fn path(attr: TokenStream, item: TokenStream) -> TokenStream { let args = resolved_path.as_mut().map(|path| mem::take(&mut path.args)); let arguments = PathOperations::resolve_path_arguments(&ast_fn.sig.inputs, args); - path_attribute - .update_parameters(arguments) - .unwrap_or_abort(); + path_attribute.update_parameters(arguments) } let path = Path::new(path_attribute, fn_name) diff --git a/utoipa-gen/src/path.rs b/utoipa-gen/src/path.rs index b762be60..e486ac74 100644 --- a/utoipa-gen/src/path.rs +++ b/utoipa-gen/src/path.rs @@ -98,7 +98,7 @@ impl ToTokens for Params<'_> { impl<'p> PathAttr<'p> { #[cfg(any(feature = "actix_extras", feature = "rocket_extras"))] - pub fn update_parameters(&mut self, arguments: Option>>) -> syn::Result<()> { + pub fn update_parameters(&mut self, arguments: Option>>) { if let Some(arguments) = arguments { if let Some(ref mut parameters) = self.params { let parameters = &mut parameters.parameters; @@ -119,8 +119,6 @@ impl<'p> PathAttr<'p> { }); } } - - Ok(()) } #[cfg(any(feature = "actix_extras", feature = "rocket_extras"))] @@ -147,8 +145,8 @@ impl<'p> PathAttr<'p> { argument.is_option, ) } - }, - Parameter::Struct(_) | Parameter::TokenStream(_) => {}, + } + Parameter::Struct(_) | Parameter::TokenStream(_) => {} }); } diff --git a/utoipa-gen/src/path/parameter.rs b/utoipa-gen/src/path/parameter.rs index c15f8e16..ad4ae34a 100644 --- a/utoipa-gen/src/path/parameter.rs +++ b/utoipa-gen/src/path/parameter.rs @@ -5,7 +5,7 @@ use quote::{quote, ToTokens}; use syn::{ parenthesized, parse::{Parse, ParseBuffer, ParseStream}, - Error, LitStr, Token, ExprPath, + Error, ExprPath, LitStr, Token, }; #[cfg(any(feature = "actix_extras", feature = "rocket_extras"))] From 4f9b1ab586820c25300f913cfc8e5e7596d588bd Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Fri, 17 Jun 2022 15:37:04 +1000 Subject: [PATCH 25/28] Update documentation for IntoParams now that ParameterIn trait has been removed. --- utoipa-gen/src/lib.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/utoipa-gen/src/lib.rs b/utoipa-gen/src/lib.rs index ded717e2..2c0e401f 100644 --- a/utoipa-gen/src/lib.rs +++ b/utoipa-gen/src/lib.rs @@ -874,10 +874,10 @@ pub fn openapi(input: TokenStream) -> TokenStream { /// /// * `style = ...` Defines how all parameters are serialized by [`ParameterStyle`][style]. Default /// values are based on _`parameter_in`_ attribute. -/// * `parameter_in = ...` = Defines where the parameters of this field are used by +/// * `parameter_in = ...` = Defines where the parameters of this field are used with a value from /// [`openapi::path::ParameterIn`][in_enum]. There is no default value, if this attribute is not -/// supplied, then one must implement [`ParameterIn`][in_trait] trait for the struct performing -/// this derive. +/// supplied, then the value is determined by the `parameter_in_provider` in +/// [`IntoParams::into_params()`](trait.IntoParams.html#tymethod.into_params). /// /// # IntoParams Field Attributes for `#[param(...)]` /// @@ -983,7 +983,6 @@ pub fn openapi(input: TokenStream) -> TokenStream { /// [struct]: https://doc.rust-lang.org/std/keyword.struct.html /// [style]: openapi/path/enum.ParameterStyle.html /// [in_enum]: utoipa/openapi/path/enum.ParameterIn.html -/// [in_trait]: utoipa/trait.ParameterIn.html /// /// [^actix]: Feature **actix_extras** need to be enabled /// From c85b06b80917eb35df8ae9a697d8c253cd644f63 Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Fri, 17 Jun 2022 16:05:58 +1000 Subject: [PATCH 26/28] Clean up attribute parsing logic to use `syn::Punctuated` --- utoipa-gen/src/schema/into_params.rs | 83 +++++++++++++--------------- 1 file changed, 39 insertions(+), 44 deletions(-) diff --git a/utoipa-gen/src/schema/into_params.rs b/utoipa-gen/src/schema/into_params.rs index 25bcd189..d918afa4 100644 --- a/utoipa-gen/src/schema/into_params.rs +++ b/utoipa-gen/src/schema/into_params.rs @@ -28,7 +28,7 @@ pub struct IntoParamsAttr { } impl IntoParamsAttr { - fn merge(&mut self, other: Self) -> &Self { + fn merge(mut self, other: Self) -> Self { if other.style.is_some() { self.style = other.style; } @@ -50,51 +50,46 @@ impl Parse for IntoParamsAttr { const EXPECTED_ATTRIBUTE: &str = "unexpected token, expected any of: names, style, parameter_in"; - let mut attributes = Self::default(); - - let mut first: bool = true; - - while !input.is_empty() { - if !first { - input.parse::()?; - } - - let ident: Ident = input.parse::().map_err(|error| { - Error::new(error.span(), format!("{EXPECTED_ATTRIBUTE}, {error}")) - })?; - - attributes.merge(match ident.to_string().as_str() { - "names" => Ok(IntoParamsAttr { - names: Some( - parse_utils::parse_punctuated_within_parenthesis::(input)? - .into_iter() - .map(|name| name.value()) - .collect(), - ), - ..IntoParamsAttr::default() - }), - "style" => { - let style: ParameterStyle = - parse_utils::parse_next(input, || input.parse::())?; - Ok(IntoParamsAttr { - style: Some(style), + let punctuated = + Punctuated::::parse_terminated_with(input, |input| { + let ident: Ident = input.parse::().map_err(|error| { + Error::new(error.span(), format!("{EXPECTED_ATTRIBUTE}, {error}")) + })?; + + Ok(match ident.to_string().as_str() { + "names" => IntoParamsAttr { + names: Some( + parse_utils::parse_punctuated_within_parenthesis::(input)? + .into_iter() + .map(|name| name.value()) + .collect(), + ), ..IntoParamsAttr::default() - }) - } - "parameter_in" => { - let parameter_in: ParameterIn = - parse_utils::parse_next(input, || input.parse::())?; - - Ok(IntoParamsAttr { - parameter_in: Some(parameter_in), - ..IntoParamsAttr::default() - }) - } - _ => Err(Error::new(ident.span(), EXPECTED_ATTRIBUTE)), - }?); + }, + "style" => { + let style: ParameterStyle = + parse_utils::parse_next(input, || input.parse::())?; + IntoParamsAttr { + style: Some(style), + ..IntoParamsAttr::default() + } + } + "parameter_in" => { + let parameter_in: ParameterIn = + parse_utils::parse_next(input, || input.parse::())?; + + IntoParamsAttr { + parameter_in: Some(parameter_in), + ..IntoParamsAttr::default() + } + } + _ => return Err(Error::new(ident.span(), EXPECTED_ATTRIBUTE)), + }) + })?; - first = false; - } + let attributes: IntoParamsAttr = punctuated + .into_iter() + .fold(IntoParamsAttr::default(), |acc, next| acc.merge(next)); Ok(attributes) } From 451ca013e8629769a54d41a6e10c2210b8b25fef Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Fri, 17 Jun 2022 20:23:53 +1000 Subject: [PATCH 27/28] Refactor ParameterIn for manual parameter definitions to use the same parsing logic, this is a breaking change to use Pascal case for these: `Path`, `Query`, `Header`, `Cookie` --- src/lib.rs | 6 +- tests/path_derive.rs | 4 +- tests/path_parameter_derive_test.rs | 16 ++--- utoipa-gen/src/lib.rs | 11 +-- utoipa-gen/src/path/parameter.rs | 105 ++++++++++++++++------------ 5 files changed, 81 insertions(+), 61 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 4a05ad25..7700eeef 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -123,7 +123,7 @@ //! (status = 404, description = "Pet was not found") //! ), //! params( -//! ("id" = u64, path, description = "Pet database id to get Pet for"), +//! ("id" = u64, Path, description = "Pet database id to get Pet for"), //! ) //! )] //! async fn get_pet_by_id(pet_id: u64) -> Pet { @@ -159,7 +159,7 @@ //! # (status = 404, description = "Pet was not found") //! # ), //! # params( -//! # ("id" = u64, path, description = "Pet database id to get Pet for"), +//! # ("id" = u64, Path, description = "Pet database id to get Pet for"), //! # ) //! # )] //! # async fn get_pet_by_id(pet_id: u64) -> Pet { @@ -344,7 +344,7 @@ pub trait Component { /// (status = 404, description = "Pet was not found") /// ), /// params( -/// ("id" = u64, path, description = "Pet database id to get Pet for"), +/// ("id" = u64, Path, description = "Pet database id to get Pet for"), /// ) /// )] /// async fn get_pet_by_id(pet_id: u64) -> Pet { diff --git a/tests/path_derive.rs b/tests/path_derive.rs index e85408b7..a72b6883 100644 --- a/tests/path_derive.rs +++ b/tests/path_derive.rs @@ -160,7 +160,7 @@ fn derive_path_with_defaults_success() { ), params( ("id" = u64, description = "Foo database id"), - ("since" = Option, query, description = "Datetime since foo is updated") + ("since" = Option, Query, description = "Datetime since foo is updated") ) )] #[allow(unused)] @@ -257,7 +257,7 @@ fn derive_path_params_intoparams() { ), params( MyParams, - ("id" = i64, path, description = "Id of some items to list") + ("id" = i64, Path, description = "Id of some items to list") ) )] #[allow(unused)] diff --git a/tests/path_parameter_derive_test.rs b/tests/path_parameter_derive_test.rs index 0d9fef14..fc10d6f2 100644 --- a/tests/path_parameter_derive_test.rs +++ b/tests/path_parameter_derive_test.rs @@ -16,7 +16,7 @@ mod derive_params_all_options { (status = 200, description = "success"), ), params( - ("id" = i32, path, deprecated, description = "Search foos by ids"), + ("id" = i32, Path, deprecated, description = "Search foos by ids"), ) )] #[allow(unused)] @@ -148,11 +148,11 @@ mod mod_derive_parameters_all_types { (status = 200, description = "success"), ), params( - ("id" = i32, path, description = "Foo id"), - ("since" = String, deprecated, query, description = "Datetime since"), - ("numbers" = Option<[u64]>, query, description = "Foo numbers list"), - ("token" = String, header, deprecated, description = "Token of foo"), - ("cookieval" = String, cookie, deprecated, description = "Foo cookie"), + ("id" = i32, Path, description = "Foo id"), + ("since" = String, deprecated, Query, description = "Datetime since"), + ("numbers" = Option<[u64]>, Query, description = "Foo numbers list"), + ("token" = String, Header, deprecated, description = "Token of foo"), + ("cookieval" = String, Cookie, deprecated, description = "Foo cookie"), ) )] #[allow(unused)] @@ -224,7 +224,7 @@ mod derive_params_without_args { (status = 200, description = "success"), ), params( - ("id" = i32, path, description = "Foo id"), + ("id" = i32, Path, description = "Foo id"), ) )] #[allow(unused)] @@ -263,7 +263,7 @@ fn derive_params_with_params_ext() { (status = 200, description = "success"), ), params( - ("value" = Option<[String]>, query, description = "Foo value description", style = Form, allow_reserved, deprecated, explode) + ("value" = Option<[String]>, Query, description = "Foo value description", style = Form, allow_reserved, deprecated, explode) ) )] #[allow(unused)] diff --git a/utoipa-gen/src/lib.rs b/utoipa-gen/src/lib.rs index 2c0e401f..04fa852e 100644 --- a/utoipa-gen/src/lib.rs +++ b/utoipa-gen/src/lib.rs @@ -455,7 +455,8 @@ pub fn derive_component(input: TokenStream) -> TokenStream { /// E.g. _`String`_ or _`[String]`_ or _`Option`_. Parameter type is placed after `name` with /// equals sign E.g. _`"id" = String`_ /// * `in` _**Must be placed after name or parameter_type**_. Define the place of the parameter. -/// E.g. _`path, query, header, cookie`_ +/// This must be one of the variants of [`openapi::path::ParameterIn`][in_enum]. +/// E.g. _`Path, Query, Header, Cookie`_ /// * `deprecated` Define whether the parameter is deprecated or not. /// * `description = "..."` Define possible description for the parameter as str. /// * `style = ...` Defines how parameters are serialized by [`ParameterStyle`][style]. Default values are based on _`in`_ attribute. @@ -609,7 +610,7 @@ pub fn derive_component(input: TokenStream) -> TokenStream { /// ), /// ), /// params( -/// ("x-csrf-token" = String, header, deprecated, description = "Current csrf token of user"), +/// ("x-csrf-token" = String, Header, deprecated, description = "Current csrf token of user"), /// ), /// security( /// (), @@ -644,7 +645,7 @@ pub fn derive_component(input: TokenStream) -> TokenStream { /// ), /// ), /// params( -/// ("x-csrf-token", header, description = "Current csrf token of user"), +/// ("x-csrf-token", Header, description = "Current csrf token of user"), /// ) /// )] /// fn post_pet(pet: Pet) -> Pet { @@ -689,6 +690,8 @@ pub fn derive_component(input: TokenStream) -> TokenStream { /// HttpResponse::Ok().json(json!({ "pet": format!("{:?}", &id.into_inner()) })) /// } /// ``` +/// +/// [in_enum]: utoipa/openapi/path/enum.ParameterIn.html /// [path]: trait.Path.html /// [openapi]: derive.OpenApi.html /// [security]: openapi/security/struct.SecurityRequirement.html @@ -914,7 +917,7 @@ pub fn openapi(input: TokenStream) -> TokenStream { /// ``` /// # Examples /// -/// Demonstrate [`IntoParams`][into_params] usage with resolving `path` and `query` parameters +/// Demonstrate [`IntoParams`][into_params] usage with resolving `Path` and `Query` parameters /// for `get_pet` endpoint. [^actix] /// ```rust /// use actix_web::{get, HttpResponse, Responder}; diff --git a/utoipa-gen/src/path/parameter.rs b/utoipa-gen/src/path/parameter.rs index ad4ae34a..e369969b 100644 --- a/utoipa-gen/src/path/parameter.rs +++ b/utoipa-gen/src/path/parameter.rs @@ -1,6 +1,6 @@ -use std::{borrow::Cow, str::FromStr}; +use std::{borrow::Cow, fmt::Display}; -use proc_macro2::{Ident, Span, TokenStream}; +use proc_macro2::{Ident, TokenStream}; use quote::{quote, ToTokens}; use syn::{ parenthesized, @@ -110,7 +110,19 @@ impl Parse for ParameterValue<'_> { } input.parse::()?; - const EXPECTED_ATTRIBUTE_MESSAGE: &str = "unexpected attribute, expected any of: path, query, header, cookie, deprecated, description, style, explode, allow_reserved, example"; + + fn expected_attribute_message() -> String { + let parameter_in_variants = ParameterIn::VARIANTS + .iter() + .map(ToString::to_string) + .collect::>() + .join(", "); + + format!( + "unexpected attribute, expected any of: {}, deprecated, description, style, explode, allow_reserved, example", + parameter_in_variants + ) + } while !input.is_empty() { let fork = input.fork(); @@ -132,28 +144,31 @@ impl Parse for ParameterValue<'_> { ext.merge(parameter_ext); } else { - let ident = input.parse::().map_err(|error| { - Error::new( - error.span(), - format!("{}, {}", EXPECTED_ATTRIBUTE_MESSAGE, error), - ) - })?; - let name = &*ident.to_string(); - - match name { - "path" | "query" | "header" | "cookie" => { - parameter.parameter_in = name - .parse::() - .map_err(|error| Error::new(ident.span(), error))?; - } - "deprecated" => parameter.deprecated = parse_utils::parse_bool_or_true(&input)?, - "description" => { - parameter.description = Some( - parse_utils::parse_next(&input, || input.parse::())?.value(), + if input.fork().parse::().is_ok() { + parameter.parameter_in = input.parse()?; + } else { + let ident = input.parse::().map_err(|error| { + Error::new( + error.span(), + format!("{}, {}", expected_attribute_message(), error), ) + })?; + let name = &*ident.to_string(); + + match name { + "deprecated" => { + parameter.deprecated = parse_utils::parse_bool_or_true(&input)? + } + "description" => { + parameter.description = Some( + parse_utils::parse_next(&input, || input.parse::())? + .value(), + ) + } + _ => return Err(Error::new(ident.span(), expected_attribute_message())), } - _ => return Err(Error::new(ident.span(), EXPECTED_ATTRIBUTE_MESSAGE)), } + if !input.is_empty() { input.parse::()?; } @@ -234,35 +249,37 @@ pub enum ParameterIn { Cookie, } -impl Default for ParameterIn { - fn default() -> Self { - Self::Path - } +impl ParameterIn { + pub const VARIANTS: &'static [Self] = &[Self::Query, Self::Path, Self::Header, Self::Cookie]; } -impl FromStr for ParameterIn { - type Err = syn::Error; - - fn from_str(s: &str) -> Result { - match s { - "path" => Ok(Self::Path), - "query" => Ok(Self::Query), - "header" => Ok(Self::Header), - "cookie" => Ok(Self::Cookie), - _ => Err(syn::Error::new( - Span::call_site(), - &format!( - "unexpected str: {}, expected one of: path, query, header, cookie", - s - ), - )), +impl Display for ParameterIn { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ParameterIn::Query => write!(f, "Query"), + ParameterIn::Path => write!(f, "Path"), + ParameterIn::Header => write!(f, "Header"), + ParameterIn::Cookie => write!(f, "Cookie"), } } } +impl Default for ParameterIn { + fn default() -> Self { + Self::Path + } +} + impl Parse for ParameterIn { fn parse(input: ParseStream) -> syn::Result { - const EXPECTED_STYLE: &str = "unexpected in, expected one of: Path, Query, Header, Cookie"; + fn expected_style() -> String { + let variants: String = ParameterIn::VARIANTS + .iter() + .map(ToString::to_string) + .collect::>() + .join(", "); + format!("unexpected in, expected one of: {}", variants) + } let style = input.parse::()?; match &*style.to_string() { @@ -270,7 +287,7 @@ impl Parse for ParameterIn { "Query" => Ok(Self::Query), "Header" => Ok(Self::Header), "Cookie" => Ok(Self::Cookie), - _ => Err(Error::new(style.span(), EXPECTED_STYLE)), + _ => Err(Error::new(style.span(), expected_style())), } } } From ce2359c2de1f6e38d0ed3ac2bb26416aadaa33c7 Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Sat, 18 Jun 2022 13:52:24 +1000 Subject: [PATCH 28/28] Updated documentation for IntoParams derive --- utoipa-gen/src/lib.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/utoipa-gen/src/lib.rs b/utoipa-gen/src/lib.rs index 04fa852e..d3476748 100644 --- a/utoipa-gen/src/lib.rs +++ b/utoipa-gen/src/lib.rs @@ -870,11 +870,12 @@ pub fn openapi(input: TokenStream) -> TokenStream { /// While it is totally okay to declare deprecated with reason /// `#[deprecated = "There is better way to do this"]` the reason would not render in OpenAPI spec. /// -/// # IntoParams Container Attributes for `#[param(...)]` +/// # IntoParams Container Attributes for `#[into_params(...)]` /// -/// The following attributes are available for use in on the container attribute `#[param(...)]` for the struct +/// The following attributes are available for use in on the container attribute `#[into_params(...)]` for the struct /// deriving `IntoParams`: /// +/// * `names(...)` Define comma seprated list of names for unnamed fields of struct used as a path parameter. /// * `style = ...` Defines how all parameters are serialized by [`ParameterStyle`][style]. Default /// values are based on _`parameter_in`_ attribute. /// * `parameter_in = ...` = Defines where the parameters of this field are used with a value from @@ -892,10 +893,6 @@ pub fn openapi(input: TokenStream) -> TokenStream { /// * `example = ...` Can be literal value, method reference or _`json!(...)`_. [^json] Given example /// will override any example in underlying parameter type. /// -/// # IntoParams Attributes for `#[into_params(...)]` -/// -/// * `names(...)` Define comma seprated list of names for unnamed fields of struct used as a path parameter. -/// /// **Note!** `#[into_params(...)]` is only supported on unnamed struct types to declare names for the arguments. /// /// Use `names` to define name for single unnamed argument.