diff --git a/utoipa-gen/CHANGELOG.md b/utoipa-gen/CHANGELOG.md index a6e757e9..dbd0f468 100644 --- a/utoipa-gen/CHANGELOG.md +++ b/utoipa-gen/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Added + +* Add `encoding` support for `request_body` (https://github.com/juhaku/utoipa/pull/1237) + ### Changed * Simplified `ToTokensDiagnostics` for `request_body` (https://github.com/juhaku/utoipa/pull/1235) diff --git a/utoipa-gen/src/path/media_type.rs b/utoipa-gen/src/path/media_type.rs index 31cd9440..29aff660 100644 --- a/utoipa-gen/src/path/media_type.rs +++ b/utoipa-gen/src/path/media_type.rs @@ -1,4 +1,5 @@ use std::borrow::Cow; +use std::collections::BTreeMap; use proc_macro2::TokenStream; use quote::{quote, ToTokens}; @@ -16,11 +17,15 @@ use crate::{parse_utils, AnyValue, Array, Diagnostics, ToTokensDiagnostics}; use super::example::Example; use super::PathTypeTree; +pub mod encoding; + +use encoding::Encoding; + /// Parse OpenAPI Media Type object params /// ( Schema ) /// ( Schema = "content/type" ) /// ( "content/type", ), -/// ( "content/type", example = ..., examples(..., ...), encoding(...) ) +/// ( "content/type", example = ..., examples(..., ...), encoding(("exampleField" = (...)), ...) ) #[derive(Default)] #[cfg_attr(feature = "debug", derive(Debug))] pub struct MediaTypeAttr<'m> { @@ -28,7 +33,7 @@ pub struct MediaTypeAttr<'m> { pub schema: Schema<'m>, pub example: Option, pub examples: Punctuated, - // econding: String, // TODO parse encoding + pub encoding: BTreeMap, } impl Parse for MediaTypeAttr<'_> { @@ -105,13 +110,41 @@ impl<'m> MediaTypeAttr<'m> { "examples" => { self.examples = parse_utils::parse_comma_separated_within_parenthesis(input)? } - // // TODO implement encoding support - // "encoding" => (), + "encoding" => { + struct KV { + k: String, + v: Encoding, + } + + impl Parse for KV { + fn parse(input: ParseStream) -> syn::Result { + let key_val; + + syn::parenthesized!(key_val in input); + + let k = key_val.parse::()?.value(); + + key_val.parse::()?; + + let v = key_val.parse::()?; + + if !key_val.is_empty() { + key_val.parse::()?; + } + + Ok(KV{k, v}) + } + } + + let fields = parse_utils::parse_comma_separated_within_parenthesis::(input)?; + + self.encoding = fields.into_iter().map(|x| (x.k, x.v)).collect(); + } unexpected => { return Err(syn::Error::new( attribute.span(), format!( - "unexpected attribute: {unexpected}, expected any of: example, examples" + "unexpected attribute: {unexpected}, expected any of: example, examples, encoding(...)" ), )) } @@ -151,12 +184,17 @@ impl ToTokensDiagnostics for MediaTypeAttr<'_> { } else { None }; + let encoding = self + .encoding + .iter() + .map(|(field_name, encoding)| quote!(.encoding(#field_name, #encoding))); tokens.extend(quote! { utoipa::openapi::content::ContentBuilder::new() #schema_tokens #example #examples + #(#encoding)* .into() }); diff --git a/utoipa-gen/src/path/media_type/encoding.rs b/utoipa-gen/src/path/media_type/encoding.rs new file mode 100644 index 00000000..c2cca0ac --- /dev/null +++ b/utoipa-gen/src/path/media_type/encoding.rs @@ -0,0 +1,90 @@ +use proc_macro2::{Ident, TokenStream}; +use quote::{quote, ToTokens}; +use syn::parse::{Parse, ParseStream}; +use syn::{parenthesized, Error, Token}; + +use crate::parse_utils; + +// (content_type = "...", explode = true, allow_reserved = false,) +#[derive(Default)] +#[cfg_attr(feature = "debug", derive(Debug))] +pub struct Encoding { + pub(super) content_type: Option, + // pub(super) headers: BTreeMap, + // pub(super) style: Option, + pub(super) explode: Option, + pub(super) allow_reserved: Option, + // pub(super) extensions: Option, +} + +impl Parse for Encoding { + fn parse(input: ParseStream) -> syn::Result { + let content; + parenthesized!(content in input); + + let mut encoding = Encoding::default(); + + while !content.is_empty() { + let ident = content.parse::()?; + let attribute_name = &*ident.to_string(); + match attribute_name { + "content_type" => { + encoding.content_type = Some( + parse_utils::parse_next_literal_str_or_expr(&content)? + ) + } + // "headers" => {} + // "style" => {} + "explode" => { + encoding.explode = Some( + parse_utils::parse_bool_or_true(&content)? + ) + } + "allow_reserved" => { + encoding.allow_reserved = Some( + parse_utils::parse_bool_or_true(&content)? + ) + } + // "extensions" => {} + _ => { + return Err( + Error::new( + ident.span(), + format!("unexpected attribute: {attribute_name}, expected one of: content_type, explode, allow_reserved") + ) + ) + } + } + + if !content.is_empty() { + content.parse::()?; + } + } + + Ok(encoding) + } +} + +impl ToTokens for Encoding { + fn to_tokens(&self, tokens: &mut TokenStream) { + let content_type = self + .content_type + .as_ref() + .map(|content_type| quote!(.content_type(Some(#content_type)))); + let explode = self + .explode + .as_ref() + .map(|value| quote!(.explode(Some(#value)))); + let allow_reserved = self + .allow_reserved + .as_ref() + .map(|allow_reserved| quote!(.allow_reserved(Some(#allow_reserved)))); + + tokens.extend(quote! { + utoipa::openapi::encoding::EncodingBuilder::new() + #content_type + #explode + #allow_reserved + }) + } +} diff --git a/utoipa-gen/src/path/request_body.rs b/utoipa-gen/src/path/request_body.rs index 37251a58..7bbf0f8d 100644 --- a/utoipa-gen/src/path/request_body.rs +++ b/utoipa-gen/src/path/request_body.rs @@ -1,7 +1,6 @@ use proc_macro2::{Ident, TokenStream}; use quote::{quote, ToTokens}; use syn::parse::ParseStream; -use syn::punctuated::Punctuated; use syn::token::Paren; use syn::{parse::Parse, Error, Token}; @@ -110,7 +109,7 @@ impl<'r> RequestBodyAttr<'r> { impl Parse for RequestBodyAttr<'_> { fn parse(input: syn::parse::ParseStream) -> syn::Result { const EXPECTED_ATTRIBUTE_MESSAGE: &str = - "unexpected attribute, expected any of: content, content_type, description, examples, example"; + "unexpected attribute, expected any of: content, content_type, description, examples, example, encoding"; let lookahead = input.lookahead1(); if lookahead.peek(Paren) { @@ -193,9 +192,7 @@ impl Parse for RequestBodyAttr<'_> { let media_type = MediaTypeAttr { schema: Schema::Default(MediaTypeAttr::parse_schema(input)?), - content_type: None, - example: None, - examples: Punctuated::default(), + ..MediaTypeAttr::default() }; Ok(RequestBodyAttr { diff --git a/utoipa-gen/src/path/response.rs b/utoipa-gen/src/path/response.rs index 177738af..e6c8ce78 100644 --- a/utoipa-gen/src/path/response.rs +++ b/utoipa-gen/src/path/response.rs @@ -360,6 +360,7 @@ impl<'r> ResponseValue<'r> { .examples .map(|(examples, _)| examples) .unwrap_or_default(), + ..MediaTypeAttr::default() }; Self { @@ -389,6 +390,7 @@ impl<'r> ResponseValue<'r> { .examples .map(|(examples, _)| examples) .unwrap_or_default(), + ..MediaTypeAttr::default() }; ResponseValue { diff --git a/utoipa-gen/src/path/response/derive.rs b/utoipa-gen/src/path/response/derive.rs index 5a04b8cd..05e3e250 100644 --- a/utoipa-gen/src/path/response/derive.rs +++ b/utoipa-gen/src/path/response/derive.rs @@ -649,6 +649,7 @@ impl<'r> EnumResponse<'r> { schema: schema.unwrap_or_else(|| Schema::Default(DefaultSchema::None)), example, examples: examples.unwrap_or_default(), + ..MediaTypeAttr::default() } }, ) diff --git a/utoipa-gen/tests/request_body_derive_test.rs b/utoipa-gen/tests/request_body_derive_test.rs index 32fa6046..6fdd4abc 100644 --- a/utoipa-gen/tests/request_body_derive_test.rs +++ b/utoipa-gen/tests/request_body_derive_test.rs @@ -386,7 +386,7 @@ fn multiple_content_with_examples() { ("example2" = (value = json!("example value"), description = "example value") ), ), ), - ( Foo = "text/xml", example = "Value" ) + ( Foo = "text/xml", example = "Value" ) ), ), responses(