utoipa_gen/path/
media_type.rs

1use std::borrow::Cow;
2use std::collections::BTreeMap;
3
4use proc_macro2::TokenStream;
5use quote::{quote, ToTokens};
6use syn::parse::{Parse, ParseStream};
7use syn::punctuated::Punctuated;
8use syn::token::{Comma, Paren};
9use syn::{Error, Generics, Ident, Token, Type};
10
11use crate::component::features::attributes::{Extensions, Inline};
12use crate::component::features::Feature;
13use crate::component::{ComponentSchema, ComponentSchemaProps, Container, TypeTree, ValueType};
14use crate::ext::ExtSchema;
15use crate::{parse_utils, AnyValue, Array, Diagnostics, ToTokensDiagnostics};
16
17use super::example::Example;
18use super::PathTypeTree;
19
20pub mod encoding;
21
22use encoding::Encoding;
23
24/// Parse OpenAPI Media Type object params
25/// ( Schema )
26/// ( Schema = "content/type" )
27/// ( "content/type", ),
28/// ( "content/type", example = ..., examples(..., ...), encoding(("exampleField" = (...)), ...), extensions(("x-ext" = json!(...))) )
29#[derive(Default)]
30#[cfg_attr(feature = "debug", derive(Debug))]
31pub struct MediaTypeAttr<'m> {
32    pub content_type: Option<parse_utils::LitStrOrExpr>, // if none, true guess
33    pub schema: Schema<'m>,
34    pub example: Option<AnyValue>,
35    pub examples: Punctuated<Example, Comma>,
36    pub encoding: BTreeMap<String, Encoding>,
37    pub extensions: Option<Extensions>,
38}
39
40impl Parse for MediaTypeAttr<'_> {
41    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
42        let mut media_type = MediaTypeAttr::default();
43
44        let fork = input.fork();
45        let is_schema = fork.parse::<DefaultSchema>().is_ok();
46        if is_schema {
47            let schema = input.parse::<DefaultSchema>()?;
48
49            let content_type = if input.parse::<Option<Token![=]>>()?.is_some() {
50                Some(
51                    input
52                        .parse::<parse_utils::LitStrOrExpr>()
53                        .map_err(|error| {
54                            Error::new(
55                                error.span(),
56                                format!(
57                                    r#"missing content type e.g. `"application/json"`, {error}"#
58                                ),
59                            )
60                        })?,
61                )
62            } else {
63                None
64            };
65            media_type.schema = Schema::Default(schema);
66            media_type.content_type = content_type;
67        } else {
68            // if schema, the content type is required
69            let content_type = input
70                .parse::<parse_utils::LitStrOrExpr>()
71                .map_err(|error| {
72                    Error::new(
73                        error.span(),
74                        format!("unexpected content, should be `schema`, `schema = content_type` or `content_type`, {error}"),
75                    )
76                })?;
77            media_type.content_type = Some(content_type);
78        }
79
80        if !input.is_empty() {
81            input.parse::<Comma>()?;
82        }
83
84        while !input.is_empty() {
85            let attribute = input.parse::<Ident>()?;
86            MediaTypeAttr::parse_named_attributes(&mut media_type, input, &attribute)?;
87        }
88
89        Ok(media_type)
90    }
91}
92
93impl<'m> MediaTypeAttr<'m> {
94    pub fn parse_schema(input: ParseStream) -> syn::Result<DefaultSchema<'m>> {
95        input.parse()
96    }
97
98    pub fn parse_named_attributes(
99        &mut self,
100        input: ParseStream,
101        attribute: &Ident,
102    ) -> syn::Result<()> {
103        let name = &*attribute.to_string();
104
105        match name {
106            "example" => {
107                self.example = Some(parse_utils::parse_next(input, || {
108                    AnyValue::parse_any(input)
109                })?)
110            }
111            "examples" => {
112                self.examples = parse_utils::parse_comma_separated_within_parenthesis(input)?
113            }
114            "encoding" => {
115                struct KV {
116                    k: String,
117                    v: Encoding,
118                }
119
120                impl Parse for KV {
121                    fn parse(input: ParseStream) -> syn::Result<Self> {
122                        let key_val;
123
124                        syn::parenthesized!(key_val in input);
125
126                        let k = key_val.parse::<syn::LitStr>()?.value();
127
128                        key_val.parse::<Token![=]>()?;
129
130                        let v = key_val.parse::<Encoding>()?;
131
132                        if !key_val.is_empty() {
133                            key_val.parse::<Comma>()?;
134                        }
135
136                        Ok(KV{k, v})
137                    }
138                }
139
140                let fields = parse_utils::parse_comma_separated_within_parenthesis::<KV>(input)?;
141
142                self.encoding = fields.into_iter().map(|x| (x.k, x.v)).collect();
143            }
144            "extensions" => {
145                self.extensions = Some(input.parse::<Extensions>()?);
146            }
147            unexpected => {
148                return Err(syn::Error::new(
149                    attribute.span(),
150                    format!(
151                        "unexpected attribute: {unexpected}, expected any of: example, examples, encoding(...), extensions(...)"
152                    ),
153                ))
154            }
155        }
156
157        if !input.is_empty() {
158            input.parse::<Comma>()?;
159        }
160
161        Ok(())
162    }
163}
164
165impl ToTokensDiagnostics for MediaTypeAttr<'_> {
166    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) -> Result<(), Diagnostics> {
167        let schema = &self.schema.try_to_token_stream()?;
168        let schema_tokens = if schema.is_empty() {
169            None
170        } else {
171            Some(quote! { .schema(Some(#schema)) })
172        };
173        let example = self
174            .example
175            .as_ref()
176            .map(|example| quote!( .example(Some(#example)) ));
177
178        let examples = self
179            .examples
180            .iter()
181            .map(|example| {
182                let name = &example.name;
183                quote!( (#name, #example) )
184            })
185            .collect::<Array<TokenStream>>();
186        let examples = if !examples.is_empty() {
187            Some(quote!( .examples_from_iter(#examples) ))
188        } else {
189            None
190        };
191        let encoding = self
192            .encoding
193            .iter()
194            .map(|(field_name, encoding)| quote!(.encoding(#field_name, #encoding)));
195        let extensions = self
196            .extensions
197            .as_ref()
198            .map(|e| quote! { .extensions(Some(#e)) });
199
200        tokens.extend(quote! {
201            utoipa::openapi::content::ContentBuilder::new()
202                #schema_tokens
203                #example
204                #examples
205                #(#encoding)*
206                #extensions
207                .into()
208        });
209
210        Ok(())
211    }
212}
213
214pub trait MediaTypePathExt<'a> {
215    fn get_component_schema(&self) -> Result<Option<ComponentSchema>, Diagnostics>;
216}
217
218#[cfg_attr(feature = "debug", derive(Debug))]
219#[allow(unused)]
220pub enum Schema<'a> {
221    Default(DefaultSchema<'a>),
222    Ext(ExtSchema<'a>),
223}
224
225impl Default for Schema<'_> {
226    fn default() -> Self {
227        Self::Default(DefaultSchema::None)
228    }
229}
230
231impl Schema<'_> {
232    pub fn get_type_tree(&self) -> Result<Option<Cow<TypeTree<'_>>>, Diagnostics> {
233        match self {
234            Self::Default(def) => def.get_type_tree(),
235            Self::Ext(ext) => ext.get_type_tree(),
236        }
237    }
238
239    pub fn get_default_content_type(&self) -> Result<Cow<'static, str>, Diagnostics> {
240        match self {
241            Self::Default(def) => def.get_default_content_type(),
242            Self::Ext(ext) => ext.get_default_content_type(),
243        }
244    }
245
246    pub fn get_component_schema(&self) -> Result<Option<ComponentSchema>, Diagnostics> {
247        match self {
248            Self::Default(def) => def.get_component_schema(),
249            Self::Ext(ext) => ext.get_component_schema(),
250        }
251    }
252
253    pub fn is_inline(&self) -> bool {
254        match self {
255            Self::Default(def) => match def {
256                DefaultSchema::TypePath(parsed) => parsed.is_inline,
257                _ => false,
258            },
259            Self::Ext(_) => false,
260        }
261    }
262}
263
264impl ToTokensDiagnostics for Schema<'_> {
265    fn to_tokens(&self, tokens: &mut TokenStream) -> Result<(), Diagnostics> {
266        match self {
267            Self::Default(def) => def.to_tokens(tokens)?,
268            Self::Ext(ext) => ext.to_tokens(tokens)?,
269        }
270
271        Ok(())
272    }
273}
274
275#[cfg_attr(feature = "debug", derive(Debug))]
276#[derive(Default)]
277pub enum DefaultSchema<'d> {
278    Ref(parse_utils::LitStrOrExpr),
279    TypePath(ParsedType<'d>),
280    /// for cases where the schema is irrelevant but we just want to return generic
281    /// `content_type` without actual schema.
282    #[default]
283    None,
284    /// Support for raw tokens as Schema. Used in response derive.
285    Raw {
286        tokens: TokenStream,
287        ty: Cow<'d, Type>,
288    },
289}
290
291impl ToTokensDiagnostics for DefaultSchema<'_> {
292    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) -> Result<(), Diagnostics> {
293        match self {
294            Self::Ref(reference) => tokens.extend(quote! {
295                utoipa::openapi::schema::Ref::new(#reference)
296            }),
297            Self::TypePath(parsed) => {
298                let is_inline = parsed.is_inline;
299                let type_tree = &parsed.to_type_tree()?;
300
301                let component_tokens = ComponentSchema::new(ComponentSchemaProps {
302                    type_tree,
303                    features: vec![Inline::from(is_inline).into()],
304                    description: None,
305                    container: &Container {
306                        generics: &Generics::default(),
307                    },
308                })?
309                .to_token_stream();
310
311                component_tokens.to_tokens(tokens);
312            }
313            Self::Raw {
314                tokens: raw_tokens, ..
315            } => {
316                raw_tokens.to_tokens(tokens);
317            }
318            // nada
319            Self::None => (),
320        }
321
322        Ok(())
323    }
324}
325
326impl<'a> MediaTypePathExt<'a> for TypeTree<'a> {
327    fn get_component_schema(&self) -> Result<Option<ComponentSchema>, Diagnostics> {
328        let generics = &if matches!(self.value_type, ValueType::Tuple) {
329            Generics::default()
330        } else {
331            self.get_path_generics()?
332        };
333
334        let component_schema = ComponentSchema::new(ComponentSchemaProps {
335            container: &Container { generics },
336            type_tree: self,
337            description: None,
338            // get the actual schema, not the reference
339            features: vec![Feature::Inline(true.into())],
340        })?;
341
342        Ok(Some(component_schema))
343    }
344}
345
346impl DefaultSchema<'_> {
347    pub fn get_default_content_type(&self) -> Result<Cow<'static, str>, Diagnostics> {
348        match self {
349            Self::TypePath(path) => {
350                let type_tree = path.to_type_tree()?;
351                Ok(type_tree.get_default_content_type())
352            }
353            Self::Ref(_) => Ok(Cow::Borrowed("application/json")),
354            Self::Raw { ty, .. } => {
355                let type_tree = TypeTree::from_type(ty.as_ref())?;
356                Ok(type_tree.get_default_content_type())
357            }
358            Self::None => Ok(Cow::Borrowed("")),
359        }
360    }
361
362    pub fn get_component_schema(&self) -> Result<Option<ComponentSchema>, Diagnostics> {
363        match self {
364            Self::TypePath(path) => {
365                let type_tree = path.to_type_tree()?;
366                let v = type_tree.get_component_schema()?;
367
368                Ok(v)
369            }
370            _ => Ok(None),
371        }
372    }
373
374    pub fn get_type_tree(&self) -> Result<Option<Cow<'_, TypeTree<'_>>>, Diagnostics> {
375        match self {
376            Self::TypePath(path) => path
377                .to_type_tree()
378                .map(|type_tree| Some(Cow::Owned(type_tree))),
379            _ => Ok(None),
380        }
381    }
382}
383
384impl Parse for DefaultSchema<'_> {
385    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
386        let fork = input.fork();
387        let is_ref = if (fork.parse::<Option<Token![ref]>>()?).is_some() {
388            fork.peek(Paren)
389        } else {
390            false
391        };
392
393        if is_ref {
394            input.parse::<Token![ref]>()?;
395            let ref_stream;
396            syn::parenthesized!(ref_stream in input);
397
398            ref_stream.parse().map(Self::Ref)
399        } else {
400            input.parse().map(Self::TypePath)
401        }
402    }
403}
404
405impl<'r> From<ParsedType<'r>> for Schema<'r> {
406    fn from(value: ParsedType<'r>) -> Self {
407        Self::Default(DefaultSchema::TypePath(value))
408    }
409}
410
411// inline(syn::TypePath) | syn::TypePath
412#[cfg_attr(feature = "debug", derive(Debug))]
413pub struct ParsedType<'i> {
414    pub ty: Cow<'i, Type>,
415    pub is_inline: bool,
416}
417
418impl ParsedType<'_> {
419    /// Get's the underlying [`syn::Type`] as [`TypeTree`].
420    fn to_type_tree(&self) -> Result<TypeTree, Diagnostics> {
421        TypeTree::from_type(&self.ty)
422    }
423}
424
425impl Parse for ParsedType<'_> {
426    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
427        let fork = input.fork();
428        let is_inline = if let Some(ident) = fork.parse::<Option<syn::Ident>>()? {
429            ident == "inline" && fork.peek(Paren)
430        } else {
431            false
432        };
433
434        let ty = if is_inline {
435            input.parse::<syn::Ident>()?;
436            let inlined;
437            syn::parenthesized!(inlined in input);
438
439            inlined.parse::<Type>()?
440        } else {
441            input.parse::<Type>()?
442        };
443
444        Ok(ParsedType {
445            ty: Cow::Owned(ty),
446            is_inline,
447        })
448    }
449}