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#[derive(Default)]
30#[cfg_attr(feature = "debug", derive(Debug))]
31pub struct MediaTypeAttr<'m> {
32 pub content_type: Option<parse_utils::LitStrOrExpr>, 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 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 #[default]
283 None,
284 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 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 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#[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 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}