utoipa_gen/path/response/
derive.rs

1use std::borrow::Cow;
2use std::{iter, mem};
3
4use proc_macro2::{Ident, Span, TokenStream};
5use proc_macro_error::{abort, emit_error};
6use quote::{quote, ToTokens};
7use syn::parse::ParseStream;
8use syn::punctuated::Punctuated;
9use syn::spanned::Spanned;
10use syn::token::Comma;
11use syn::{
12    Attribute, Data, Field, Fields, Generics, Lifetime, LifetimeParam, LitStr, Path, Type,
13    TypePath, Variant,
14};
15
16use crate::component::schema::{EnumSchema, NamedStructSchema};
17use crate::doc_comment::CommentAttributes;
18use crate::path::{InlineType, PathType};
19use crate::{Array, ResultExt};
20
21use super::{
22    Content, DeriveIntoResponsesValue, DeriveResponseValue, DeriveResponsesAttributes,
23    DeriveToResponseValue, ResponseTuple, ResponseTupleInner, ResponseValue,
24};
25
26pub struct ToResponse<'r> {
27    ident: Ident,
28    lifetime: Lifetime,
29    generics: Generics,
30    response: ResponseTuple<'r>,
31}
32
33impl<'r> ToResponse<'r> {
34    const LIFETIME: &'static str = "'__r";
35
36    pub fn new(
37        attributes: Vec<Attribute>,
38        data: &'r Data,
39        generics: Generics,
40        ident: Ident,
41    ) -> ToResponse<'r> {
42        let response = match &data {
43            Data::Struct(struct_value) => match &struct_value.fields {
44                Fields::Named(fields) => {
45                    ToResponseNamedStructResponse::new(&attributes, &ident, &fields.named).0
46                }
47                Fields::Unnamed(fields) => {
48                    let field = fields
49                        .unnamed
50                        .iter()
51                        .next()
52                        .expect("Unnamed struct must have 1 field");
53
54                    ToResponseUnnamedStructResponse::new(&attributes, &field.ty, &field.attrs).0
55                }
56                Fields::Unit => ToResponseUnitStructResponse::new(&attributes).0,
57            },
58            Data::Enum(enum_value) => {
59                EnumResponse::new(&ident, &enum_value.variants, &attributes).0
60            }
61            Data::Union(_) => abort!(ident, "`ToResponse` does not support `Union` type"),
62        };
63
64        let lifetime = Lifetime::new(ToResponse::LIFETIME, Span::call_site());
65
66        Self {
67            ident,
68            lifetime,
69            generics,
70            response,
71        }
72    }
73}
74
75impl ToTokens for ToResponse<'_> {
76    fn to_tokens(&self, tokens: &mut TokenStream) {
77        let (_, ty_generics, where_clause) = self.generics.split_for_impl();
78
79        let lifetime = &self.lifetime;
80        let ident = &self.ident;
81        let name = ident.to_string();
82        let response = &self.response;
83
84        let mut to_reponse_generics = self.generics.clone();
85        to_reponse_generics
86            .params
87            .push(syn::GenericParam::Lifetime(LifetimeParam::new(
88                lifetime.clone(),
89            )));
90        let (to_response_impl_generics, _, _) = to_reponse_generics.split_for_impl();
91
92        tokens.extend(quote! {
93            impl #to_response_impl_generics utoipa::ToResponse <#lifetime> for #ident #ty_generics #where_clause {
94                fn response() -> (& #lifetime str, utoipa::openapi::RefOr<utoipa::openapi::response::Response>) {
95                    (#name, #response.into())
96                }
97            }
98        });
99    }
100}
101
102pub struct IntoResponses {
103    pub attributes: Vec<Attribute>,
104    pub data: Data,
105    pub generics: Generics,
106    pub ident: Ident,
107}
108
109impl ToTokens for IntoResponses {
110    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
111        let responses = match &self.data {
112            Data::Struct(struct_value) => match &struct_value.fields {
113                Fields::Named(fields) => {
114                    let response =
115                        NamedStructResponse::new(&self.attributes, &self.ident, &fields.named).0;
116                    let status = &response.status_code;
117
118                    Array::from_iter(iter::once(quote!((#status, #response))))
119                }
120                Fields::Unnamed(fields) => {
121                    let field = fields
122                        .unnamed
123                        .iter()
124                        .next()
125                        .expect("Unnamed struct must have 1 field");
126
127                    let response =
128                        UnnamedStructResponse::new(&self.attributes, &field.ty, &field.attrs).0;
129                    let status = &response.status_code;
130
131                    Array::from_iter(iter::once(quote!((#status, #response))))
132                }
133                Fields::Unit => {
134                    let response = UnitStructResponse::new(&self.attributes).0;
135                    let status = &response.status_code;
136
137                    Array::from_iter(iter::once(quote!((#status, #response))))
138                }
139            },
140            Data::Enum(enum_value) => enum_value
141                .variants
142                .iter()
143                .map(|variant| match &variant.fields {
144                    Fields::Named(fields) => {
145                        NamedStructResponse::new(&variant.attrs, &variant.ident, &fields.named).0
146                    }
147                    Fields::Unnamed(fields) => {
148                        let field = fields
149                            .unnamed
150                            .iter()
151                            .next()
152                            .expect("Unnamed enum variant must have 1 field");
153                        UnnamedStructResponse::new(&variant.attrs, &field.ty, &field.attrs).0
154                    }
155                    Fields::Unit => UnitStructResponse::new(&variant.attrs).0,
156                })
157                .map(|response| {
158                    let status = &response.status_code;
159                    quote!((#status, utoipa::openapi::RefOr::from(#response)))
160                })
161                .collect::<Array<TokenStream>>(),
162            Data::Union(_) => abort!(self.ident, "`IntoResponses` does not support `Union` type"),
163        };
164
165        let ident = &self.ident;
166        let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl();
167
168        let responses = if responses.len() > 0 {
169            Some(quote!( .responses_from_iter(#responses)))
170        } else {
171            None
172        };
173        tokens.extend(quote!{
174            impl #impl_generics utoipa::IntoResponses for #ident #ty_generics #where_clause {
175                fn responses() -> std::collections::BTreeMap<String, utoipa::openapi::RefOr<utoipa::openapi::response::Response>> {
176                    utoipa::openapi::response::ResponsesBuilder::new()
177                        #responses
178                        .build()
179                        .into()
180                }
181            }
182        })
183    }
184}
185
186trait Response {
187    fn to_type(ident: &Ident) -> Type {
188        let path = Path::from(ident.clone());
189        let type_path = TypePath { path, qself: None };
190        Type::Path(type_path)
191    }
192
193    fn has_no_field_attributes(attribute: &Attribute) -> (bool, &'static str) {
194        const ERROR: &str =
195            "Unexpected field attribute, field attributes are only supported at unnamed fields";
196
197        let ident = attribute.path().get_ident().unwrap();
198        match &*ident.to_string() {
199            "to_schema" => (false, ERROR),
200            "ref_response" => (false, ERROR),
201            "content" => (false, ERROR),
202            "to_response" => (false, ERROR),
203            _ => (true, ERROR),
204        }
205    }
206
207    fn validate_attributes<'a, I: IntoIterator<Item = &'a Attribute>>(
208        attributes: I,
209        validate: impl Fn(&Attribute) -> (bool, &'static str),
210    ) {
211        for attribute in attributes {
212            let (valid, message) = validate(attribute);
213            if !valid {
214                emit_error!(attribute, message)
215            }
216        }
217    }
218}
219
220struct UnnamedStructResponse<'u>(ResponseTuple<'u>);
221
222impl Response for UnnamedStructResponse<'_> {}
223
224impl<'u> UnnamedStructResponse<'u> {
225    fn new(attributes: &[Attribute], ty: &'u Type, inner_attributes: &[Attribute]) -> Self {
226        let is_inline = inner_attributes
227            .iter()
228            .any(|attribute| attribute.path().get_ident().unwrap() == "to_schema");
229        let ref_response = inner_attributes
230            .iter()
231            .any(|attribute| attribute.path().get_ident().unwrap() == "ref_response");
232        let to_response = inner_attributes
233            .iter()
234            .any(|attribute| attribute.path().get_ident().unwrap() == "to_response");
235
236        if is_inline && (ref_response || to_response) {
237            abort!(
238                ty.span(),
239                "Attribute `to_schema` cannot be used with `ref_response` and `to_response` attribute"
240            )
241        }
242        let mut derive_value = DeriveIntoResponsesValue::from_attributes(attributes)
243            .expect("`IntoResponses` must have `#[response(...)]` attribute");
244        let description = CommentAttributes::from_attributes(attributes).as_formatted_string();
245        let status_code = mem::take(&mut derive_value.status);
246
247        match (ref_response, to_response) {
248            (false, false) => Self(
249                (
250                    status_code,
251                    ResponseValue::from_derive_into_responses_value(derive_value, description)
252                        .response_type(Some(PathType::MediaType(InlineType {
253                            ty: Cow::Borrowed(ty),
254                            is_inline,
255                        }))),
256                )
257                    .into(),
258            ),
259            (true, false) => Self(ResponseTuple {
260                inner: Some(ResponseTupleInner::Ref(InlineType {
261                    ty: Cow::Borrowed(ty),
262                    is_inline: false,
263                })),
264                status_code,
265            }),
266            (false, true) => Self(ResponseTuple {
267                inner: Some(ResponseTupleInner::Ref(InlineType {
268                    ty: Cow::Borrowed(ty),
269                    is_inline: true,
270                })),
271                status_code,
272            }),
273            (true, true) => {
274                abort!(
275                    ty.span(),
276                    "Cannot define `ref_response` and `to_response` attribute simultaneously"
277                );
278            }
279        }
280    }
281}
282
283struct NamedStructResponse<'n>(ResponseTuple<'n>);
284
285impl Response for NamedStructResponse<'_> {}
286
287impl NamedStructResponse<'_> {
288    fn new(attributes: &[Attribute], ident: &Ident, fields: &Punctuated<Field, Comma>) -> Self {
289        Self::validate_attributes(attributes, Self::has_no_field_attributes);
290        Self::validate_attributes(
291            fields.iter().flat_map(|field| &field.attrs),
292            Self::has_no_field_attributes,
293        );
294
295        let mut derive_value = DeriveIntoResponsesValue::from_attributes(attributes)
296            .expect("`IntoResponses` must have `#[response(...)]` attribute");
297        let description = CommentAttributes::from_attributes(attributes).as_formatted_string();
298        let status_code = mem::take(&mut derive_value.status);
299
300        let inline_schema = NamedStructSchema {
301            attributes,
302            fields,
303            aliases: None,
304            features: None,
305            generics: None,
306            rename_all: None,
307            struct_name: Cow::Owned(ident.to_string()),
308            schema_as: None,
309        };
310
311        let ty = Self::to_type(ident);
312
313        Self(
314            (
315                status_code,
316                ResponseValue::from_derive_into_responses_value(derive_value, description)
317                    .response_type(Some(PathType::InlineSchema(
318                        inline_schema.to_token_stream(),
319                        ty,
320                    ))),
321            )
322                .into(),
323        )
324    }
325}
326
327struct UnitStructResponse<'u>(ResponseTuple<'u>);
328
329impl Response for UnitStructResponse<'_> {}
330
331impl UnitStructResponse<'_> {
332    fn new(attributes: &[Attribute]) -> Self {
333        Self::validate_attributes(attributes, Self::has_no_field_attributes);
334
335        let mut derive_value = DeriveIntoResponsesValue::from_attributes(attributes)
336            .expect("`IntoResponses` must have `#[response(...)]` attribute");
337        let status_code = mem::take(&mut derive_value.status);
338        let description = CommentAttributes::from_attributes(attributes).as_formatted_string();
339
340        Self(
341            (
342                status_code,
343                ResponseValue::from_derive_into_responses_value(derive_value, description),
344            )
345                .into(),
346        )
347    }
348}
349
350struct ToResponseNamedStructResponse<'p>(ResponseTuple<'p>);
351
352impl Response for ToResponseNamedStructResponse<'_> {}
353
354impl<'p> ToResponseNamedStructResponse<'p> {
355    fn new(attributes: &[Attribute], ident: &Ident, fields: &Punctuated<Field, Comma>) -> Self {
356        Self::validate_attributes(attributes, Self::has_no_field_attributes);
357        Self::validate_attributes(
358            fields.iter().flat_map(|field| &field.attrs),
359            Self::has_no_field_attributes,
360        );
361
362        let derive_value = DeriveToResponseValue::from_attributes(attributes);
363        let description = CommentAttributes::from_attributes(attributes).as_formatted_string();
364        let ty = Self::to_type(ident);
365
366        let inline_schema = NamedStructSchema {
367            aliases: None,
368            fields,
369            features: None,
370            generics: None,
371            attributes,
372            struct_name: Cow::Owned(ident.to_string()),
373            rename_all: None,
374            schema_as: None,
375        };
376        let response_type = PathType::InlineSchema(inline_schema.to_token_stream(), ty);
377
378        let mut response_value: ResponseValue = ResponseValue::from(DeriveResponsesAttributes {
379            derive_value,
380            description,
381        });
382        response_value.response_type = Some(response_type);
383
384        Self(response_value.into())
385    }
386}
387
388struct ToResponseUnnamedStructResponse<'c>(ResponseTuple<'c>);
389
390impl Response for ToResponseUnnamedStructResponse<'_> {}
391
392impl<'u> ToResponseUnnamedStructResponse<'u> {
393    fn new(attributes: &[Attribute], ty: &'u Type, inner_attributes: &[Attribute]) -> Self {
394        Self::validate_attributes(attributes, Self::has_no_field_attributes);
395        Self::validate_attributes(inner_attributes, |attribute| {
396            const ERROR: &str =
397                "Unexpected attribute, `content` is only supported on unnamed field enum variant";
398            if attribute.path().get_ident().unwrap() == "content" {
399                (false, ERROR)
400            } else {
401                (true, ERROR)
402            }
403        });
404        let derive_value = DeriveToResponseValue::from_attributes(attributes);
405        let description = CommentAttributes::from_attributes(attributes).as_formatted_string();
406
407        let is_inline = inner_attributes
408            .iter()
409            .any(|attribute| attribute.path().get_ident().unwrap() == "to_schema");
410        let mut response_value: ResponseValue = ResponseValue::from(DeriveResponsesAttributes {
411            description,
412            derive_value,
413        });
414
415        response_value.response_type = Some(PathType::MediaType(InlineType {
416            ty: Cow::Borrowed(ty),
417            is_inline,
418        }));
419
420        Self(response_value.into())
421    }
422}
423
424struct VariantAttributes<'r> {
425    type_and_content: Option<(&'r Type, String)>,
426    derive_value: Option<DeriveToResponseValue>,
427    is_inline: bool,
428}
429
430struct EnumResponse<'r>(ResponseTuple<'r>);
431
432impl Response for EnumResponse<'_> {}
433
434impl<'r> EnumResponse<'r> {
435    fn new(
436        ident: &Ident,
437        variants: &'r Punctuated<Variant, Comma>,
438        attributes: &[Attribute],
439    ) -> Self {
440        Self::validate_attributes(attributes, Self::has_no_field_attributes);
441        Self::validate_attributes(
442            variants.iter().flat_map(|variant| &variant.attrs),
443            Self::has_no_field_attributes,
444        );
445
446        let ty = Self::to_type(ident);
447        let description = CommentAttributes::from_attributes(attributes).as_formatted_string();
448
449        let variants_content = variants
450            .into_iter()
451            .map(Self::parse_variant_attributes)
452            .filter_map(Self::to_content);
453        let content: Punctuated<Content, Comma> = Punctuated::from_iter(variants_content);
454
455        let derive_value = DeriveToResponseValue::from_attributes(attributes);
456        if let Some(derive_value) = &derive_value {
457            if (!content.is_empty() && derive_value.example.is_some())
458                || (!content.is_empty() && derive_value.examples.is_some())
459            {
460                let ident = derive_value
461                    .example
462                    .as_ref()
463                    .map(|(_, ident)| ident)
464                    .or_else(|| derive_value.examples.as_ref().map(|(_, ident)| ident))
465                    .expect("Expected `example` or `examples` to be present");
466                abort! {
467                    ident,
468                    "Enum with `#[content]` attribute in variant cannot have enum level `example` or `examples` defined";
469                    help = "Try defining `{}` on the enum variant", ident.to_string(),
470                }
471            }
472        }
473
474        let mut response_value: ResponseValue = From::from(DeriveResponsesAttributes {
475            derive_value,
476            description,
477        });
478        response_value.response_type = if content.is_empty() {
479            let inline_schema =
480                EnumSchema::new(Cow::Owned(ident.to_string()), variants, attributes);
481
482            Some(PathType::InlineSchema(
483                inline_schema.into_token_stream(),
484                ty,
485            ))
486        } else {
487            None
488        };
489        response_value.content = content;
490
491        Self(response_value.into())
492    }
493
494    fn parse_variant_attributes(variant: &Variant) -> VariantAttributes {
495        let variant_derive_response_value =
496            DeriveToResponseValue::from_attributes(variant.attrs.as_slice());
497        // named enum variant should not have field attributes
498        if let Fields::Named(named_fields) = &variant.fields {
499            Self::validate_attributes(
500                named_fields.named.iter().flat_map(|field| &field.attrs),
501                Self::has_no_field_attributes,
502            )
503        };
504
505        let field = variant.fields.iter().next();
506
507        let content_type = field.and_then(|field| {
508            field
509                .attrs
510                .iter()
511                .find(|attribute| attribute.path().get_ident().unwrap() == "content")
512                .map(|attribute| {
513                    attribute
514                        .parse_args_with(|input: ParseStream| input.parse::<LitStr>())
515                        .unwrap_or_abort()
516                })
517                .map(|content| content.value())
518        });
519
520        let is_inline = field
521            .map(|field| {
522                field
523                    .attrs
524                    .iter()
525                    .any(|attribute| attribute.path().get_ident().unwrap() == "to_schema")
526            })
527            .unwrap_or(false);
528
529        VariantAttributes {
530            type_and_content: field.map(|field| &field.ty).zip(content_type),
531            derive_value: variant_derive_response_value,
532            is_inline,
533        }
534    }
535
536    fn to_content(
537        VariantAttributes {
538            type_and_content: field_and_content,
539            mut derive_value,
540            is_inline,
541        }: VariantAttributes,
542    ) -> Option<Content<'_>> {
543        let (example, examples) = if let Some(variant_derive) = &mut derive_value {
544            (
545                mem::take(&mut variant_derive.example),
546                mem::take(&mut variant_derive.examples),
547            )
548        } else {
549            (None, None)
550        };
551
552        field_and_content.map(|(ty, content_type)| {
553            Content(
554                content_type,
555                PathType::MediaType(InlineType {
556                    ty: Cow::Borrowed(ty),
557                    is_inline,
558                }),
559                example.map(|(example, _)| example),
560                examples.map(|(examples, _)| examples),
561            )
562        })
563    }
564}
565
566struct ToResponseUnitStructResponse<'u>(ResponseTuple<'u>);
567
568impl Response for ToResponseUnitStructResponse<'_> {}
569
570impl ToResponseUnitStructResponse<'_> {
571    fn new(attributes: &[Attribute]) -> Self {
572        Self::validate_attributes(attributes, Self::has_no_field_attributes);
573
574        let derive_value = DeriveToResponseValue::from_attributes(attributes);
575        let description = CommentAttributes::from_attributes(attributes).as_formatted_string();
576        let response_value: ResponseValue = ResponseValue::from(DeriveResponsesAttributes {
577            derive_value,
578            description,
579        });
580
581        Self(response_value.into())
582    }
583}