diff --git a/Cargo.toml b/Cargo.toml index cfbc2686..0fca0487 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,8 +38,8 @@ 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" chrono = { version = "0.4", features = ["serde"] } rust_decimal = "1" diff --git a/examples/todo-warp/src/main.rs b/examples/todo-warp/src/main.rs index b0ae63eb..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::Component; + use utoipa::{Component, IntoParams}; use warp::{hyper::StatusCode, Filter, Reply}; pub type Store = Arc>>; @@ -110,6 +112,14 @@ mod todo { value: String, } + #[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, + } + pub fn handlers() -> impl Filter + Clone { let store = Store::default(); @@ -117,6 +127,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 +157,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..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 { @@ -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]. @@ -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() @@ -531,7 +533,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/tests/path_derive.rs b/tests/path_derive.rs index 3480db40..a72b6883 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; @@ -157,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)] @@ -231,6 +234,91 @@ fn derive_path_with_security_requirements() { } } +#[test] +fn derive_path_params_intoparams() { + #[derive(serde::Deserialize, IntoParams)] + #[into_params(style = Form, parameter_in = Query)] + struct MyParams { + /// Foo database id. + #[param(example = 1)] + #[allow(unused)] + id: u64, + /// Datetime since foo is updated. + #[param(example = "2020-04-12T10:23:00Z")] + #[allow(unused)] + since: Option, + } + + #[utoipa::path( + get, + path = "/list/{id}", + responses( + (status = 200, description = "success response") + ), + params( + MyParams, + ("id" = i64, Path, description = "Id of some items to list") + ) + )] + #[allow(unused)] + fn list(id: i64, 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/{id}" + }; + + let parameters = operation.get("parameters").unwrap(); + + assert_json_eq!( + parameters, + json!([ + { + "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" + }, + { + "deprecated": false, + "description": "Id of some items to list", + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "int64", + "type": "integer" + } + } + ]) + ) +} + #[cfg(feature = "uuid")] #[test] fn derive_path_with_uuid() { 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 c5d0137c..d3476748 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}; @@ -404,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 @@ -436,12 +442,21 @@ pub fn derive_component(input: TokenStream) -> TokenStream { /// /// # Params Attributes /// +/// The list of attributes inside the `params(...)` attribute can take two forms: [Tuples](#tuples) or [IntoParams +/// Type](#intoparams-type). +/// +/// ## Tuples +/// +/// 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. /// * `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 /// 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. @@ -450,12 +465,47 @@ 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"), -/// ("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 +/// +/// 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. +/// +/// [into_params]: ./trait.IntoParams.html +/// **For example:** +/// +/// ```text +/// params(MyParameters) +/// ``` +/// +/// 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 @@ -560,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( /// (), @@ -595,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 { @@ -640,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 @@ -677,7 +729,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); + path_attribute.update_parameters(arguments) } let path = Path::new(path_attribute, fn_name) @@ -799,7 +851,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. @@ -819,18 +870,29 @@ 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 `#[into_params(...)]` /// -/// * `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 `#[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 +/// [`openapi::path::ParameterIn`][in_enum]. There is no default value, if this attribute is not +/// 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(...)]` +/// +/// 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 /// 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. @@ -852,7 +914,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}; @@ -888,28 +950,57 @@ pub fn openapi(input: TokenStream) -> TokenStream { /// } /// ``` /// +/// 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 PetQuery { +/// /// Name of pet +/// name: Option, +/// /// Age of pet +/// age: Option, +/// } +/// +/// #[utoipa::path( +/// get, +/// path = "/get_pet", +/// params(PetQuery), +/// responses( +/// (status = 200, description = "success response") +/// ) +/// )] +/// async fn get_pet(query: PetQuery) { +/// // ... +/// } +/// ``` +/// /// [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 /// /// [^actix]: Feature **actix_extras** need to be enabled /// /// [^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() @@ -1263,11 +1354,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 +1368,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 +1383,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 +1393,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..e486ac74 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::punctuated::Punctuated; use syn::{parenthesized, parse::Parse, Token}; use crate::{component_type::ComponentType, security_requirement::SecurityRequirementAttr, Array}; @@ -64,28 +65,58 @@ 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. This is parsed from the +/// `#[utoipa::path(params(...))]` attribute. +#[cfg_attr(feature = "debug", derive(Debug))] +#[derive(Default)] +struct Params<'p> { + /// A list of tuples of attributes that defines a parameter. + pub parameters: Vec>, +} + +impl Parse for Params<'_> { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + 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)); + } +} + impl<'p> PathAttr<'p> { #[cfg(any(feature = "actix_extras", feature = "rocket_extras"))] 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; 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)); - self.params = Some(params); + .for_each(|parameter| parameters.push(parameter)); + self.params = Some(Params { + parameters, + ..Params::default() + }); } } } @@ -98,25 +129,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(_) => {} }); } @@ -187,7 +217,7 @@ impl Parse for PathAttr<'_> { "params" => { let params; parenthesized!(params in input); - path_attr.params = Some(parse_utils::parse_groups(¶ms)?); + path_attr.params = Some(params.parse()?); } "tag" => { path_attr.tag = Some(parse_utils::parse_next_literal_str(input)?); @@ -451,12 +481,13 @@ impl ToTokens for Path<'_> { } } +#[cfg_attr(feature = "debug", derive(Debug))] struct Operation<'a> { operation_id: &'a String, 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,13 +543,8 @@ 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))}), - }); - } + 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 ee13745a..e369969b 100644 --- a/utoipa-gen/src/path/parameter.rs +++ b/utoipa-gen/src/path/parameter.rs @@ -1,15 +1,19 @@ -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::{ - parse::{Parse, ParseStream}, - Error, LitStr, Token, + parenthesized, + parse::{Parse, ParseBuffer, ParseStream}, + Error, ExprPath, LitStr, Token, }; #[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::FieldParamContainerAttributes, AnyValue, Deprecated, + Required, Type, +}; use super::property::Property; @@ -27,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(ExprPath), #[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> { @@ -54,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) { @@ -85,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(), @@ -99,7 +110,19 @@ impl Parse for Parameter<'_> { } 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(); @@ -121,90 +144,104 @@ impl Parse for Parameter<'_> { 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::()?; } } } - Ok(Parameter::Value(parameter)) + Ok(parameter) } } -impl ToTokens for Parameter<'_> { +impl ToTokens for ParameterValue<'_> { fn to_tokens(&self, tokens: &mut TokenStream) { - let mut handle_single_parameter = |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(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), } } } #[cfg_attr(feature = "debug", derive(Debug))] -#[derive(PartialEq)] +#[derive(PartialEq, Clone, Copy)] pub enum ParameterIn { Query, Path, @@ -212,28 +249,45 @@ pub enum ParameterIn { Cookie, } +impl ParameterIn { + pub const VARIANTS: &'static [Self] = &[Self::Query, Self::Path, Self::Header, Self::Cookie]; +} + +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 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 Parse for ParameterIn { + fn parse(input: ParseStream) -> syn::Result { + 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() { + "Path" => Ok(Self::Path), + "Query" => Ok(Self::Query), + "Header" => Ok(Self::Header), + "Cookie" => Ok(Self::Cookie), + _ => Err(Error::new(style.span(), expected_style())), } } } @@ -260,8 +314,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 +397,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.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; diff --git a/utoipa-gen/src/schema/into_params.rs b/utoipa-gen/src/schema/into_params.rs index aac4aff4..d918afa4 100644 --- a/utoipa-gen/src/schema/into_params.rs +++ b/utoipa-gen/src/schema/into_params.rs @@ -2,54 +2,109 @@ 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::{ component_type::{ComponentFormat, ComponentType}, doc_comment::CommentAttributes, parse_utils, - path::parameter::ParameterExt, + path::parameter::{ParameterExt, ParameterIn, ParameterStyle}, Array, Required, }; use super::{ComponentPart, GenericType, ValueType}; +/// Container attribute `#[into_params(...)]`. +#[derive(Default)] #[cfg_attr(feature = "debug", derive(Debug))] pub struct IntoParamsAttr { - names: Vec, + /// See [`ParameterStyle`]. + style: Option, + /// Specify names of unnamed fields with `names(...) attribute.` + names: Option>, + /// See [`ParameterIn`]. + parameter_in: Option, +} + +impl IntoParamsAttr { + fn merge(mut self, other: Self) -> Self { + if other.style.is_some() { + self.style = other.style; + } + + if other.names.is_some() { + 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: 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, parameter_in"; + + 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() + }, + "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)), + }) })?; - Ok(IntoParamsAttr { - names: parse_utils::parse_punctuated_within_parenthesis::(input)? - .into_iter() - .map(|name| name.value()) - .collect(), - }) + let attributes: IntoParamsAttr = punctuated + .into_iter() + .fold(IntoParamsAttr::default(), |acc, next| acc.merge(next)); + + 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 { @@ -57,7 +112,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")) @@ -66,27 +121,34 @@ 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)| { - 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), + name: into_params_attrs + .as_ref() + .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(...) container attribute for tuple struct field {}", + index + ))), + parameter_in: into_params_attrs.as_ref().and_then(|attrs| attrs.parameter_in), + }, + } }) .collect::>(); 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() } - } }); } @@ -147,7 +209,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, } } @@ -155,25 +217,57 @@ 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>, + /// See [`IntoParamsAttr::parameter_in`]. + pub parameter_in: Option, +} + +#[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)) - .unwrap_or_default(); + .or_else(|| self.container_attributes.name.cloned()) + .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(); 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()) + } + }, + ); + + tokens.extend(quote! { .required(#required) }); @@ -187,25 +281,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);