Skip to content

Commit

Permalink
[utoipa-gen] MediaTypeAttr, Encoding: Parse encodings
Browse files Browse the repository at this point in the history
Though only in a limited fashion - notable fields are still not
implemented.

Though this does let you do:

```rs
   request_body(
       content(...)
       encoding(
           ("field_name", (
               content_type = "application/json",
               explode = true,
               allow_reserved = true,
           ))
       )
   )
```

Partially addresses #1087
  • Loading branch information
jsoo1 committed Dec 17, 2024
1 parent b9b67f7 commit c46c7e5
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 11 deletions.
4 changes: 4 additions & 0 deletions utoipa-gen/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Added

* Add `encoding` support for `request_body` (https://github.com/juhaku/utoipa/pull/1237)

### Fixed

* Fix tagged enum with flatten fields (https://github.com/juhaku/utoipa/pull/1208)
Expand Down
48 changes: 43 additions & 5 deletions utoipa-gen/src/path/media_type.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::borrow::Cow;
use std::collections::BTreeMap;

use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
Expand All @@ -16,19 +17,23 @@ 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> {
pub content_type: Option<parse_utils::LitStrOrExpr>, // if none, true guess
pub schema: Schema<'m>,
pub example: Option<AnyValue>,
pub examples: Punctuated<Example, Comma>,
// econding: String, // TODO parse encoding
pub encoding: BTreeMap<String, Encoding>,
}

impl Parse for MediaTypeAttr<'_> {
Expand Down Expand Up @@ -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<Self> {
let key_val;

syn::parenthesized!(key_val in input);

let k = key_val.parse::<syn::LitStr>()?.value();

key_val.parse::<Token![=]>()?;

let v = key_val.parse::<Encoding>()?;

if !key_val.is_empty() {
key_val.parse::<Comma>()?;
}

Ok(KV{k, v})
}
}

let fields = parse_utils::parse_comma_separated_within_parenthesis::<KV>(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(...)"
),
))
}
Expand Down Expand Up @@ -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()
});

Expand Down
90 changes: 90 additions & 0 deletions utoipa-gen/src/path/media_type/encoding.rs
Original file line number Diff line number Diff line change
@@ -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<parse_utils::LitStrOrExpr>,
// pub(super) headers: BTreeMap<String, Header>,
// pub(super) style: Option<ParameterStyle>,
pub(super) explode: Option<bool>,
pub(super) allow_reserved: Option<bool>,
// pub(super) extensions: Option<Extensions>,
}

impl Parse for Encoding {
fn parse(input: ParseStream) -> syn::Result<Self> {
let content;
parenthesized!(content in input);

let mut encoding = Encoding::default();

while !content.is_empty() {
let ident = content.parse::<Ident>()?;
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::<Token![,]>()?;
}
}

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
})
}
}
7 changes: 2 additions & 5 deletions utoipa-gen/src/path/request_body.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand Down Expand Up @@ -110,7 +109,7 @@ impl<'r> RequestBodyAttr<'r> {
impl Parse for RequestBodyAttr<'_> {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
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) {
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions utoipa-gen/src/path/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,7 @@ impl<'r> ResponseValue<'r> {
.examples
.map(|(examples, _)| examples)
.unwrap_or_default(),
..MediaTypeAttr::default()
};

Self {
Expand Down Expand Up @@ -389,6 +390,7 @@ impl<'r> ResponseValue<'r> {
.examples
.map(|(examples, _)| examples)
.unwrap_or_default(),
..MediaTypeAttr::default()
};

ResponseValue {
Expand Down
1 change: 1 addition & 0 deletions utoipa-gen/src/path/response/derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
},
)
Expand Down
2 changes: 1 addition & 1 deletion utoipa-gen/tests/request_body_derive_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down

0 comments on commit c46c7e5

Please sign in to comment.