Skip to main content

paperclip_macros/
actix.rs

1//! Convenience macros for the [actix-web](https://github.com/paperclip-rs/paperclip/tree/master/plugins/actix-web)
2//! OpenAPI plugin (exposed by paperclip with `actix` feature).
3
4use heck::*;
5use http::StatusCode;
6use lazy_static::lazy_static;
7use proc_macro::TokenStream;
8use quote::{quote, ToTokens};
9use strum_macros::EnumString;
10use syn::{
11    parse_macro_input,
12    punctuated::{Pair, Punctuated},
13    spanned::Spanned,
14    Attribute, Data, DataEnum, DeriveInput, Field, Fields, FieldsNamed, FieldsUnnamed, FnArg,
15    Generics, Ident, ItemFn, Lit, Meta, MetaList, MetaNameValue, NestedMeta, Path, PathArguments,
16    ReturnType, Token, TraitBound, Type, TypeTraitObject,
17};
18
19use proc_macro2::TokenStream as TokenStream2;
20use std::collections::HashMap;
21
22const SCHEMA_MACRO_ATTR: &str = "openapi";
23
24lazy_static! {
25    static ref EMPTY_SCHEMA_HELP: String = format!(
26        "you can mark the struct with #[{}(empty)] to ignore this warning.",
27        SCHEMA_MACRO_ATTR
28    );
29}
30
31/// Actual parser and emitter for `api_v2_operation` macro.
32pub fn emit_v2_operation(attrs: TokenStream, input: TokenStream) -> TokenStream {
33    let default_span = proc_macro2::Span::call_site();
34    let mut item_ast: ItemFn = match syn::parse(input) {
35        Ok(s) => s,
36        Err(e) => {
37            emit_error!(e.span().unwrap(), "operation must be a function.");
38            return quote!().into();
39        }
40    };
41
42    // Unit struct
43    let s_name = format!("paperclip_{}", item_ast.sig.ident);
44    let unit_struct = Ident::new(&s_name, default_span);
45    let generics = &item_ast.sig.generics;
46    let mut generics_call = quote!();
47    let mut struct_definition = quote!(
48        #[allow(non_camel_case_types, missing_docs)]
49        struct #unit_struct;
50    );
51    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
52    if !generics.params.is_empty() {
53        let turbofish = ty_generics.as_turbofish();
54        let generics_params = extract_generics_params(&item_ast);
55        generics_call = quote!(#turbofish { p: std::marker::PhantomData });
56        struct_definition = quote!(struct #unit_struct #ty_generics { p: std::marker::PhantomData<(#generics_params)> } )
57    }
58
59    // Get rid of async prefix. In the end, we'll have them all as `impl Future` thingies.
60    if item_ast.sig.asyncness.is_some() {
61        item_ast.sig.asyncness = None;
62    }
63
64    let mut wrapper = quote!(paperclip::actix::ResponseWrapper<actix_web::HttpResponse, #unit_struct #ty_generics>);
65    let mut is_impl_trait = false;
66    let mut is_responder = false;
67    match &mut item_ast.sig.output {
68        rt @ ReturnType::Default => {
69            // Not particularly useful, but let's deal with it anyway
70            *rt = ReturnType::Type(
71                Token![->](default_span),
72                Box::new(syn::parse2(wrapper.clone()).expect("parsing empty type")),
73            );
74        }
75        ReturnType::Type(_, ty) => {
76            let t = quote!(#ty).to_string();
77            if let Type::ImplTrait(_) = &**ty {
78                is_impl_trait = true;
79            }
80
81            if t == "impl Responder" {
82                // `impl Responder` is a special case because we have to add another wrapper.
83                // FIXME: Better way to deal with this?
84                is_responder = true;
85                *ty = syn::parse2(quote!(
86                    impl std::future::Future<Output=paperclip::actix::ResponderWrapper<#ty>>
87                ))
88                .expect("parsing impl trait");
89            } else if !is_impl_trait {
90                // Any handler that's not returning an impl trait should return an `impl Future`
91                *ty = syn::parse2(quote!(impl std::future::Future<Output=#ty>))
92                    .expect("parsing impl trait");
93            }
94
95            if let Type::ImplTrait(imp) = &**ty {
96                let obj = TypeTraitObject {
97                    dyn_token: Some(Token![dyn](default_span)),
98                    bounds: imp.bounds.clone(),
99                };
100                *ty = syn::parse2(quote!(#ty + paperclip::v2::schema::Apiv2Operation))
101                    .expect("parsing impl trait");
102
103                if !is_responder {
104                    // NOTE: We're only using the box "type" to generate the operation data, we're not boxing
105                    // the handlers at runtime.
106                    wrapper = quote!(paperclip::actix::ResponseWrapper<Box<#obj + std::marker::Unpin>, #unit_struct #ty_generics>);
107                }
108            }
109        }
110    }
111
112    let block = item_ast.block;
113    // We need a function because devs should be able to use "return" keyword along the way.
114    let wrapped_fn_call = if is_responder {
115        quote!(paperclip::util::ready(paperclip::actix::ResponderWrapper((move || #block)())))
116    } else if is_impl_trait {
117        quote!((move || #block)())
118    } else {
119        quote!((move || async move #block)())
120    };
121
122    item_ast.block = Box::new(
123        syn::parse2(quote!(
124            {
125                let f = #wrapped_fn_call;
126                paperclip::actix::ResponseWrapper {
127                    responder: f,
128                    operations: #unit_struct #generics_call,
129                }
130            }
131        ))
132        .expect("parsing wrapped block"),
133    );
134
135    // Initialize operation parameters from macro attributes
136    let (mut op_params, mut op_values) = parse_operation_attrs(attrs);
137
138    if op_params.iter().any(|i| *i == "skip") {
139        return quote!(
140            #[allow(non_camel_case_types, missing_docs)]
141            #struct_definition
142
143            #item_ast
144
145            impl #impl_generics paperclip::v2::schema::Apiv2Operation for #unit_struct #ty_generics #where_clause {
146                fn operation() -> paperclip::v2::models::DefaultOperationRaw {
147                    Default::default()
148                }
149
150                #[allow(unused_mut)]
151                fn security_definitions() -> std::collections::BTreeMap<String, paperclip::v2::models::SecurityScheme> {
152                    Default::default()
153                }
154
155                fn definitions() -> std::collections::BTreeMap<String, paperclip::v2::models::DefaultSchemaRaw> {
156                    Default::default()
157                }
158
159                fn is_visible() -> bool {
160                    false
161                }
162            }
163        ).into();
164    }
165
166    // Optionally extract summary and description from doc comments
167    if !op_params.iter().any(|i| *i == "summary") {
168        let (summary, description) = extract_fn_documentation(&item_ast);
169        if let Some(summary) = summary {
170            op_params.push(Ident::new("summary", item_ast.span()));
171            op_values.push(summary)
172        }
173        if let Some(description) = description {
174            op_params.push(Ident::new("description", item_ast.span()));
175            op_values.push(description)
176        }
177    }
178
179    if op_params.iter().any(|i| *i == "deprecated") || extract_deprecated(&item_ast.attrs) {
180        op_params.push(Ident::new("deprecated", item_ast.span()));
181        op_values.push(quote!(true))
182    }
183
184    let modifiers = extract_fn_arguments_types(&item_ast);
185
186    let operation_modifier = if is_responder {
187        quote! { paperclip::actix::ResponderWrapper::<actix_web::HttpResponse> }
188    } else {
189        quote! { <<#wrapper as std::future::Future>::Output> }
190    };
191
192    quote!(
193        #struct_definition
194
195        #item_ast
196
197        impl #impl_generics paperclip::v2::schema::Apiv2Operation for #unit_struct #ty_generics #where_clause {
198            fn operation() -> paperclip::v2::models::DefaultOperationRaw {
199                use paperclip::actix::OperationModifier;
200                let mut op = paperclip::v2::models::DefaultOperationRaw {
201                    #(
202                        #op_params: #op_values,
203                    )*
204                    .. Default::default()
205                };
206                #(
207                    <#modifiers>::update_parameter(&mut op);
208                    <#modifiers>::update_security(&mut op);
209                )*
210                #operation_modifier::update_response(&mut op);
211                op
212            }
213
214            #[allow(unused_mut)]
215            fn security_definitions() -> std::collections::BTreeMap<String, paperclip::v2::models::SecurityScheme> {
216                use paperclip::actix::OperationModifier;
217                let mut map = Default::default();
218                #(
219                    <#modifiers>::update_security_definitions(&mut map);
220                )*
221                map
222            }
223
224            fn definitions() -> std::collections::BTreeMap<String, paperclip::v2::models::DefaultSchemaRaw> {
225                use paperclip::actix::OperationModifier;
226                let mut map = std::collections::BTreeMap::new();
227                #(
228                    <#modifiers>::update_definitions(&mut map);
229                )*
230                #operation_modifier::update_definitions(&mut map);
231                map
232            }
233        }
234    )
235        .into()
236}
237
238/// Extract punctuated generic parameters from fn definition
239fn extract_generics_params(item_ast: &ItemFn) -> Punctuated<Ident, syn::token::Comma> {
240    item_ast
241        .sig
242        .generics
243        .params
244        .pairs()
245        .filter_map(|pair| match pair {
246            Pair::Punctuated(syn::GenericParam::Type(gen), punct) => {
247                Some(Pair::new(gen.ident.clone(), Some(*punct)))
248            }
249            Pair::End(syn::GenericParam::Type(gen)) => Some(Pair::new(gen.ident.clone(), None)),
250            _ => None,
251        })
252        .collect()
253}
254
255/// Extract function arguments
256fn extract_fn_arguments_types(item_ast: &ItemFn) -> Vec<Type> {
257    item_ast
258        .sig
259        .inputs
260        .iter()
261        .filter_map(|inp| match inp {
262            FnArg::Receiver(_) => None,
263            FnArg::Typed(ref t) => Some(*t.ty.clone()),
264        })
265        .collect()
266}
267
268/// Parse macro attrs, matching to Operation fields
269/// Returning operation attribute identifier and value initialization arrays
270/// Note: Array likes initialized from string "val1, val2, val3", where "val1"
271/// would parse into destination item
272fn parse_operation_attrs(attrs: TokenStream) -> (Vec<Ident>, Vec<proc_macro2::TokenStream>) {
273    let attrs = crate::parse_input_attrs(attrs);
274    let mut params = Vec::new();
275    let mut values = Vec::new();
276    for attr in attrs.0 {
277        match &attr {
278            NestedMeta::Meta(Meta::Path(attr_path)) => {
279                if let Some(attr_) = attr_path.get_ident() {
280                    if *attr_ == "skip" || *attr_ == "deprecated" {
281                        params.push(attr_.clone());
282                    } else {
283                        emit_error!(attr_.span(), "Not supported bare attribute {:?}", attr_)
284                    }
285                }
286            }
287            NestedMeta::Meta(Meta::NameValue(MetaNameValue { path, lit, .. })) => {
288                if let Some(ident) = path.get_ident() {
289                    match ident.to_string().as_str() {
290                        "summary" | "description" | "operation_id" => {
291                            if let Lit::Str(val) = lit {
292                                params.push(ident.clone());
293                                values.push(quote!(Some(# val.to_string())));
294                            } else {
295                                emit_error!(lit.span(), "Expected string literal: {:?}", lit)
296                            }
297                        }
298                        "consumes" | "produces" => {
299                            if let Lit::Str(mimes) = lit {
300                                let mut mime_types = Vec::new();
301                                for val in mimes.value().split(',') {
302                                    let val = val.trim();
303                                    if let Err(err) = val.parse::<mime::Mime>() {
304                                        emit_error!(
305                                            lit.span(),
306                                            "Value {} does not parse as mime type: {}",
307                                            val,
308                                            err
309                                        );
310                                    } else {
311                                        mime_types.push(quote!(paperclip::v2::models::MediaRange( # val.parse().unwrap())));
312                                    }
313                                }
314                                if !mime_types.is_empty() {
315                                    params.push(ident.clone());
316                                    values.push(quote!({
317                                    let mut tmp = std::collections::BTreeSet::new();
318                                    # (
319                                    tmp.insert(# mime_types);
320                                    ) *
321                                    Some(tmp)
322                                    }));
323                                }
324                            } else {
325                                emit_error!(
326                                    lit.span(),
327                                    "Expected comma separated values in string literal: {:?}",
328                                    lit
329                                )
330                            }
331                        }
332                        x => emit_error!(ident.span(), "Unknown attribute {}", x),
333                    }
334                } else {
335                    emit_error!(
336                        path.span(),
337                        "Expected single identifier, got path {:?}",
338                        path
339                    )
340                }
341            }
342            NestedMeta::Meta(Meta::List(MetaList { path, nested, .. })) => {
343                if let Some(ident) = path.get_ident() {
344                    match ident.to_string().as_str() {
345                        "tags" => {
346                            let mut tags = Vec::new();
347                            for meta in nested.pairs().map(|pair| pair.into_value()) {
348                                if let NestedMeta::Meta(Meta::Path(Path { segments, .. })) = meta {
349                                    tags.push(segments[0].ident.to_string());
350                                } else if let NestedMeta::Lit(Lit::Str(lit)) = meta {
351                                    tags.push(lit.value());
352                                } else {
353                                    emit_error!(
354                                        meta.span(),
355                                        "Expected comma separated list of tags idents: {:?}",
356                                        meta
357                                    )
358                                }
359                            }
360                            if !tags.is_empty() {
361                                params.push(ident.clone());
362                                values.push(quote!(vec![ #( #tags.to_string() ),* ]));
363                            }
364                        }
365                        x => emit_error!(ident.span(), "Unknown list ident {}", x),
366                    }
367                }
368            }
369            _ => {
370                emit_error!(attr.span(), "Not supported attribute type {:?}", attr)
371            }
372        }
373    }
374    (params, values)
375}
376
377/// Extracts summary from top line doc comment and description from the rest
378fn extract_fn_documentation(
379    item_ast: &ItemFn,
380) -> (
381    Option<proc_macro2::TokenStream>,
382    Option<proc_macro2::TokenStream>,
383) {
384    let docs = extract_documentation(&item_ast.attrs);
385    let lines = docs.lines();
386    let mut before_empty = true;
387    let (summary, description): (Vec<_>, Vec<_>) = lines.partition(|line| {
388        if line.trim().is_empty() {
389            before_empty = false
390        };
391        before_empty
392    });
393    let none_if_empty = |text: &str| {
394        if text.is_empty() {
395            None
396        } else {
397            Some(quote!(Some(#text.to_string())))
398        }
399    };
400    let summary = none_if_empty(summary.into_iter().collect::<String>().trim());
401    let description = none_if_empty(description.join("\n").trim());
402    (summary, description)
403}
404
405/// Actual parser and emitter for `api_v2_errors` macro.
406pub fn emit_v2_errors(attrs: TokenStream, input: TokenStream) -> TokenStream {
407    let item_ast = match crate::expect_struct_or_enum(input) {
408        Ok(i) => i,
409        Err(ts) => return ts,
410    };
411
412    let name = &item_ast.ident;
413    let attrs = crate::parse_input_attrs(attrs);
414    let generics = item_ast.generics.clone();
415    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
416
417    let mut default_schema: Option<syn::Ident> = None;
418    // Convert macro attributes to tuples in form of (u16, &str, &Option<syn::Ident>)
419    let error_codes = attrs
420        .0
421        .iter()
422        // Pair code attrs with description attrs; save attr itself to properly span error messages at later stage
423        .fold(Vec::new(), |mut list: Vec<(Option<u16>, Option<String>, Option<syn::Ident>, _)>, attr| {
424            let span = attr.span().unwrap();
425            match attr {
426                // Read named attribute.
427                NestedMeta::Meta(Meta::NameValue(name_value)) => {
428                    let attr_name = name_value.path.get_ident().map(|ident| ident.to_string());
429                    let attr_value = &name_value.lit;
430                    match (attr_name.as_deref(), attr_value) {
431                        // "code" attribute adds new element to list
432                        (Some("code"), Lit::Int(attr_value)) => {
433                            let status_code = attr_value.base10_parse::<u16>()
434                                .map_err(|_| emit_error!(span, "Invalid u16 in code argument")).ok();
435                            list.push((status_code, None, None, attr));
436                        }
437                        // "description" attribute updates last element in list
438                        (Some("description"), Lit::Str(attr_value)) =>
439                            if let Some(last_value) = list.last_mut() {
440                                if last_value.1.is_some() {
441                                    emit_warning!(span, "This attribute overwrites previous description");
442                                }
443                                last_value.1 = Some(attr_value.value());
444                            } else {
445                                emit_error!(span, "Attribute 'description' can be only placed after prior 'code' argument");
446                            },
447                        // "schema" attribute updates last element in list
448                        (Some("schema"), Lit::Str(attr_value)) =>
449                            if let Some(last_value) = list.last_mut() {
450                                if last_value.2.is_some() {
451                                    emit_warning!(span, "This attribute overwrites previous schema");
452                                }
453                                match attr_value.parse() {
454                                    Ok(value) => last_value.2 = Some(value),
455                                    Err(error) => emit_error!(span, "Error parsing schema: {}", error),
456                                }
457                            } else {
458                                emit_error!(span, "Attribute 'schema' can be only placed after prior 'code' argument");
459                            },
460                        (Some("default_schema"), Lit::Str(attr_value)) =>
461                            match attr_value.parse() {
462                                Ok(value) => default_schema = Some(value),
463                                Err(error) => emit_error!(span, "Error parsing default_schema: {}", error),
464                            },
465                        _ => emit_error!(span, "Invalid macro attribute. Should be plain u16, 'code = u16', 'description = str', 'schema = str' or 'default_schema = str'")
466                    }
467                }
468                // Read plain status code as attribute.
469                NestedMeta::Lit(Lit::Int(attr_value)) => {
470                    let status_code = attr_value.base10_parse::<u16>()
471                        .map_err(|_| emit_error!(span, "Invalid u16 in code argument")).ok();
472                    list.push((status_code, None, None, attr));
473                }
474                _ => emit_error!(span, "This macro supports only named attributes - 'code' (u16), 'description' (str), 'schema' (str) or 'default_schema' (str)")
475            }
476
477            list
478        })
479        .iter()
480        // Map code-message pairs into bits of code, filter empty codes out
481        .filter_map(|quad| {
482            let (code, description, schema) = match quad {
483                (Some(code), Some(description), schema, _) => {
484                    (code, description.to_owned(), schema.to_owned())
485                }
486                (Some(code), None, schema, attr) => {
487                    let span = attr.span().unwrap();
488                    let description = StatusCode::from_u16(*code)
489                        .map_err(|_| {
490                            emit_warning!(span, format!("Invalid status code {}", code));
491                            String::new()
492                        })
493                        .map(|s| s.canonical_reason()
494                            .map(str::to_string)
495                            .unwrap_or_else(|| {
496                                emit_warning!(span, format!("Status code {} doesn't have a canonical name", code));
497                                String::new()
498                            })
499                        )
500                        .unwrap_or_else(|_| String::new());
501                    (code, description, schema.to_owned())
502                }
503                (None, _, _, _) => return None,
504            };
505            Some((*code, description, schema))
506        })
507        .collect::<Vec<(u16, String, Option<syn::Ident>)>>();
508
509    let error_definitions = error_codes.iter().fold(
510        if default_schema.is_none() {
511            TokenStream2::new()
512        } else {
513            quote! {
514                #default_schema::update_definitions(map);
515            }
516        },
517        |mut stream, (_, _, schema)| {
518            if let Some(schema) = schema {
519                let tokens = quote! {
520                    #schema::update_definitions(map);
521                };
522                stream.extend(tokens);
523            }
524            stream
525        },
526    );
527
528    let update_definitions = quote! {
529        fn update_definitions(map: &mut std::collections::BTreeMap<String, paperclip::v2::models::DefaultSchemaRaw>) {
530            use paperclip::actix::OperationModifier;
531            #error_definitions
532        }
533    };
534
535    // for compatibility with previous error trait
536    let error_map = error_codes.iter().fold(
537        proc_macro2::TokenStream::new(),
538        |mut stream, (code, description, _)| {
539            let token = quote! {
540                (#code, #description),
541            };
542            stream.extend(token);
543            stream
544        },
545    );
546
547    let update_error_helper = quote! {
548        fn update_error_definitions(code: &u16, description: &str, schema: &Option<&str>, op: &mut paperclip::v2::models::DefaultOperationRaw) {
549            if let Some(schema) = &schema {
550                op.responses.insert(code.to_string(), paperclip::v2::models::Either::Right(paperclip::v2::models::Response {
551                    description: Some(description.to_string()),
552                    schema: Some(paperclip::v2::models::DefaultSchemaRaw {
553                        name: Some(schema.to_string()),
554                        reference: Some(format!("#/definitions/{}", schema)),
555                        .. Default::default()
556                    }),
557                    ..Default::default()
558                }));
559            } else {
560                op.responses.insert(code.to_string(), paperclip::v2::models::Either::Right(paperclip::v2::models::DefaultResponseRaw {
561                    description: Some(description.to_string()),
562                    ..Default::default()
563                }));
564            }
565        }
566    };
567    let default_schema = default_schema.map(|i| i.to_string());
568    let update_errors = error_codes.iter().fold(
569        update_error_helper,
570        |mut stream, (code, description, schema)| {
571            let tokens = if let Some(schema) = schema {
572                let schema = schema.to_string();
573                quote! {
574                    update_error_definitions(&#code, #description, &Some(#schema), op);
575                }
576            } else if let Some(scheme) = &default_schema {
577                quote! {
578                    update_error_definitions(&#code, #description, &Some(#scheme), op);
579                }
580            } else {
581                quote! {
582                    update_error_definitions(&#code, #description, &None, op);
583                }
584            };
585            stream.extend(tokens);
586            stream
587        },
588    );
589
590    let gen = quote! {
591        #item_ast
592
593        impl #impl_generics paperclip::v2::schema::Apiv2Errors for #name #ty_generics #where_clause {
594            const ERROR_MAP: &'static [(u16, &'static str)] = &[
595                #error_map
596            ];
597            fn update_error_definitions(op: &mut paperclip::v2::models::DefaultOperationRaw) {
598                #update_errors
599            }
600            #update_definitions
601        }
602    };
603
604    gen.into()
605}
606
607/// Actual parser and emitter for `emit_v2_errors_overlay` macro.
608pub fn emit_v2_errors_overlay(attrs: TokenStream, input: TokenStream) -> TokenStream {
609    let item_ast = match crate::expect_struct_or_enum(input) {
610        Ok(i) => i,
611        Err(ts) => return ts,
612    };
613
614    let name = &item_ast.ident;
615    let inner = match &item_ast.data {
616        Data::Struct(s) => if s.fields.len() == 1 {
617            match &s.fields {
618                Fields::Unnamed(s) => s.unnamed.first().map(|s| match &s.ty {
619                    Type::Path(s) => s.path.segments.first().map(|f| &f.ident),
620                    _ => None,
621                }),
622                _ => None,
623            }
624        } else {
625            None
626        }
627        .flatten()
628        .unwrap_or_else(|| {
629            abort!(
630                s.fields.span(),
631                "This macro supports only unnamed structs with 1 element"
632            )
633        }),
634        _ => {
635            abort!(item_ast.span(), "This macro supports only unnamed structs");
636        }
637    };
638
639    let attrs = crate::parse_input_attrs(attrs);
640    let generics = item_ast.generics.clone();
641    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
642
643    // Convert macro attributes to vector of u16
644    let error_codes = attrs
645        .0
646        .iter()
647        // Pair code attrs with description attrs; save attr itself to properly span error messages at later stage
648        .fold(Vec::new(), |mut list: Vec<u16>, attr| {
649            let span = attr.span().unwrap();
650            match attr {
651                // Read plain status code as attribute.
652                NestedMeta::Lit(Lit::Int(attr_value)) => {
653                    let status_code = attr_value
654                        .base10_parse::<u16>()
655                        .map_err(|_| emit_error!(span, "Invalid u16 in code argument"))
656                        .unwrap();
657                    list.push(status_code);
658                }
659                _ => emit_error!(
660                    span,
661                    "This macro supports only named attributes - 'code' (u16)"
662                ),
663            }
664
665            list
666        });
667    let filter_error_codes = error_codes
668        .iter()
669        .fold(TokenStream2::new(), |mut stream, code| {
670            let status_code = &code.to_string();
671            let tokens = quote! {
672                op.responses.remove(#status_code);
673            };
674            stream.extend(tokens);
675            stream
676        });
677
678    let gen = quote! {
679        #item_ast
680
681        impl std::fmt::Display for #name {
682            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
683                std::fmt::Display::fmt(&self.0, f)
684            }
685        }
686
687        impl actix_web::error::ResponseError for #name {
688            fn status_code(&self) -> actix_web::http::StatusCode {
689                self.0.status_code()
690            }
691            fn error_response(&self) -> actix_web::HttpResponse {
692                self.0.error_response()
693            }
694        }
695
696        impl #impl_generics paperclip::v2::schema::Apiv2Errors for #name #ty_generics #where_clause {
697            const ERROR_MAP: &'static [(u16, &'static str)] = &[];
698            fn update_definitions(map: &mut std::collections::BTreeMap<String, paperclip::v2::models::DefaultSchemaRaw>) {
699                #inner::update_definitions(map);
700            }
701            fn update_error_definitions(op: &mut paperclip::v2::models::DefaultOperationRaw) {
702                #inner::update_error_definitions(op);
703                #filter_error_codes
704            }
705        }
706    };
707
708    gen.into()
709}
710
711fn extract_rename(attrs: &[Attribute]) -> Option<String> {
712    let attrs = extract_openapi_attrs(attrs);
713    for attr in attrs.flat_map(|attr| attr.into_iter()) {
714        if let NestedMeta::Meta(Meta::NameValue(nv)) = attr {
715            if nv.path.is_ident("rename") {
716                if let Lit::Str(s) = nv.lit {
717                    return Some(s.value());
718                } else {
719                    emit_error!(
720                        nv.lit.span().unwrap(),
721                        format!(
722                            "`#[{}(rename = \"...\")]` expects a string argument",
723                            SCHEMA_MACRO_ATTR
724                        ),
725                    );
726                }
727            }
728        }
729    }
730
731    None
732}
733
734fn extract_example(attrs: &[Attribute]) -> Option<String> {
735    let attrs = extract_openapi_attrs(attrs);
736    for attr in attrs.flat_map(|attr| attr.into_iter()) {
737        if let NestedMeta::Meta(Meta::NameValue(nv)) = attr {
738            if nv.path.is_ident("example") {
739                if let Lit::Str(s) = nv.lit {
740                    return Some(s.value());
741                } else {
742                    emit_error!(
743                        nv.lit.span().unwrap(),
744                        format!(
745                            "`#[{}(example = \"...\")]` expects a string argument",
746                            SCHEMA_MACRO_ATTR
747                        ),
748                    );
749                }
750            }
751        }
752    }
753
754    None
755}
756
757fn field_extract_f32(nv: MetaNameValue) -> Option<proc_macro2::TokenStream> {
758    let value: Result<proc_macro2::TokenStream, String> = match &nv.lit {
759        Lit::Str(s) => match s.value().parse::<f32>() {
760            Ok(s) => Ok(quote! { #s }),
761            Err(error) => Err(error.to_string()),
762        },
763        Lit::Float(f) => Ok(quote! { #f }),
764        Lit::Int(i) => {
765            let f: f32 = i
766                .base10_parse()
767                .unwrap_or_else(|e| abort!(i.span(), "{}", e));
768            Ok(quote! { #f })
769        }
770        _ => {
771            emit_error!(
772                nv.lit.span().unwrap(),
773                "Expected a string, float or int argument"
774            );
775            return None;
776        }
777    };
778    match value {
779        Ok(value) => Some(value),
780        Err(error) => {
781            emit_error!(nv.lit.span().unwrap(), error);
782            None
783        }
784    }
785}
786
787fn extract_openapi_f32(attrs: &[Attribute], ident: &str) -> Option<proc_macro2::TokenStream> {
788    let attrs = extract_openapi_attrs(attrs);
789    for attr in attrs.flat_map(|attr| attr.into_iter()) {
790        if let NestedMeta::Meta(Meta::NameValue(nv)) = attr {
791            if nv.path.is_ident(ident) {
792                return field_extract_f32(nv);
793            }
794        }
795    }
796
797    None
798}
799
800/// Actual parser and emitter for `api_v2_schema` macro.
801pub fn emit_v2_definition(input: TokenStream) -> TokenStream {
802    let item_ast = match crate::expect_struct_or_enum(input) {
803        Ok(i) => i,
804        Err(ts) => return ts,
805    };
806
807    if let Some(empty) = check_empty_schema(&item_ast) {
808        return empty;
809    }
810
811    let docs = extract_documentation(&item_ast.attrs);
812    let docs = docs.trim();
813
814    let example = if let Some(example) = extract_example(&item_ast.attrs) {
815        // allow to parse escaped json string or single str value
816        quote!(
817            paperclip::v2::serde_json::from_str::<paperclip::v2::serde_json::Value>(#example).ok().or_else(|| Some(#example.into()))
818        )
819    } else {
820        quote!(None)
821    };
822
823    let props = SerdeProps::from_item_attrs(&item_ast.attrs);
824
825    let name = &item_ast.ident;
826
827    // Add `Apiv2Schema` bound for impl if the type is generic.
828    let mut generics = item_ast.generics.clone();
829    let bound = syn::parse2::<TraitBound>(quote!(paperclip::v2::schema::Apiv2Schema))
830        .expect("expected to parse trait bound");
831    generics.type_params_mut().for_each(|param| {
832        param.bounds.push(bound.clone().into());
833    });
834
835    let opt_impl = add_optional_impl(name, &generics);
836    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
837
838    // FIXME: Use attr path segments to find flattening, skipping, etc.
839    let mut props_gen = quote! {};
840
841    match &item_ast.data {
842        Data::Struct(ref s) => {
843            props_gen.extend(quote!(
844                schema.data_type = Some(DataType::Object);
845            ));
846            match &s.fields {
847                Fields::Named(ref f) => {
848                    handle_field_struct(f, &item_ast.attrs, &props, &mut props_gen)
849                }
850                Fields::Unnamed(ref f) => {
851                    handle_unnamed_field_struct(f, &item_ast.attrs, &mut props_gen)
852                }
853                Fields::Unit => {
854                    emit_warning!(
855                        s.struct_token.span().unwrap(),
856                        "unit structs do not have any fields and hence will have empty schema.";
857                        help = "{}", &*EMPTY_SCHEMA_HELP;
858                    );
859                }
860            }
861        }
862        Data::Enum(ref e) => handle_enum(e, &props, &mut props_gen),
863        Data::Union(ref u) => emit_error!(
864            u.union_token.span().unwrap(),
865            "unions are unsupported for deriving schema"
866        ),
867    };
868
869    let base_name = extract_rename(&item_ast.attrs).unwrap_or_else(|| name.to_string());
870    let type_params: Vec<&Ident> = generics.type_params().map(|p| &p.ident).collect();
871    let schema_name = if type_params.is_empty() {
872        quote! { #base_name }
873    } else {
874        let type_names = quote! {
875            [#(#type_params::name()),*]
876                .iter()
877                .filter_map(|n| n.to_owned())
878                .collect::<Vec<String>>()
879                .join(", ")
880        };
881        quote! { format!("{}<{}>", #base_name, #type_names) }
882    };
883    let props_gen_empty = props_gen.is_empty();
884
885    #[cfg(not(feature = "path-in-definition"))]
886    let default_schema_raw_def = quote! {
887        let mut schema = DefaultSchemaRaw {
888            name: Some(#schema_name.into()),
889            example: #example,
890            ..Default::default()
891        };
892    };
893
894    #[cfg(feature = "path-in-definition")]
895    let default_schema_raw_def = quote! {
896        let mut schema = DefaultSchemaRaw {
897            name: Some(Self::__paperclip_schema_name()), // Add name for later use.
898            example: #example,
899            .. Default::default()
900        };
901    };
902
903    #[cfg(not(feature = "path-in-definition"))]
904    let paperclip_schema_name_def = quote!();
905
906    #[cfg(feature = "path-in-definition")]
907    let paperclip_schema_name_def = quote! {
908        fn __paperclip_schema_name() -> String {
909            // The module path itself, e.g cratename::module
910            let full_module_path = std::module_path!().to_string();
911            // We're not interested in the crate name, nor do we want :: as a seperator
912            let trimmed_module_path = full_module_path.split("::")
913                .enumerate()
914                .filter(|(index, _)| *index != 0) // Skip the first element, i.e the crate name
915                .map(|(_, component)| component)
916                .collect::<Vec<_>>()
917                .join("_");
918            format!("{}_{}", trimmed_module_path, #schema_name)
919        }
920    };
921
922    #[cfg(not(feature = "path-in-definition"))]
923    let const_name_def = quote! {
924        fn name() -> Option<String> {
925            Some(#schema_name.to_string())
926        }
927    };
928
929    #[cfg(feature = "path-in-definition")]
930    let const_name_def = quote!();
931
932    #[cfg(not(feature = "path-in-definition"))]
933    let props_gen_empty_name_def = quote! {
934        schema.name = Some(#schema_name.into());
935    };
936
937    #[cfg(feature = "path-in-definition")]
938    let props_gen_empty_name_def = quote! {
939        schema.name = Some(Self::__paperclip_schema_name());
940    };
941
942    let gen = quote! {
943        impl #impl_generics #name #ty_generics #where_clause {
944            #paperclip_schema_name_def
945        }
946
947        impl #impl_generics paperclip::v2::schema::Apiv2Schema for #name #ty_generics #where_clause {
948            #const_name_def
949            fn description() -> &'static str {
950                #docs
951            }
952
953            fn raw_schema() -> paperclip::v2::models::DefaultSchemaRaw {
954                use paperclip::v2::models::{DataType, DataTypeFormat, DefaultSchemaRaw};
955                use paperclip::v2::schema::TypedData;
956
957                #default_schema_raw_def
958
959                #props_gen
960                // props_gen may override the schema for unnamed structs with 1 element
961                // as it replaces the struct type with inner type.
962                // make sure we set the name properly if props_gen is not empty
963                if !#props_gen_empty {
964                    #props_gen_empty_name_def
965                }
966                schema
967            }
968        }
969
970        #opt_impl
971    };
972
973    gen.into()
974}
975
976/// Actual parser and emitter for `Apiv2Security` derive macro.
977pub fn emit_v2_security(input: TokenStream) -> TokenStream {
978    let item_ast = match crate::expect_struct_or_enum(input) {
979        Ok(i) => i,
980        Err(ts) => return ts,
981    };
982
983    if let Some(empty) = check_empty_schema(&item_ast) {
984        return empty;
985    }
986
987    let name = &item_ast.ident;
988    // Add `Apiv2Schema` bound for impl if the type is generic.
989    let mut generics = item_ast.generics.clone();
990    let bound = syn::parse2::<TraitBound>(quote!(paperclip::v2::schema::Apiv2Schema))
991        .expect("expected to parse trait bound");
992    generics.type_params_mut().for_each(|param| {
993        param.bounds.push(bound.clone().into());
994    });
995
996    let opt_impl = add_optional_impl(name, &generics);
997    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
998
999    let mut security_attrs = HashMap::new();
1000    let mut scopes = Vec::new();
1001
1002    let valid_attrs = vec![
1003        "alias",
1004        "description",
1005        "name",
1006        "in",
1007        "flow",
1008        "auth_url",
1009        "token_url",
1010        "parent",
1011    ];
1012    let invalid_attr_msg = format!("Invalid macro attribute. Should be bare security type [\"apiKey\", \"oauth2\"] or named attribute {:?}", valid_attrs);
1013
1014    // Read security params from openapi attr.
1015    for nested in extract_openapi_attrs(&item_ast.attrs) {
1016        for nested_attr in nested {
1017            let span = nested_attr.span().unwrap();
1018            match &nested_attr {
1019                // Read bare attribute.
1020                NestedMeta::Meta(Meta::Path(attr_path)) => {
1021                    if let Some(type_) = attr_path.get_ident() {
1022                        if security_attrs
1023                            .insert("type".to_string(), type_.to_string())
1024                            .is_some()
1025                        {
1026                            emit_warning!(span, "Auth type defined multiple times.");
1027                        }
1028                    }
1029                }
1030                // Read named attribute.
1031                NestedMeta::Meta(Meta::NameValue(name_value)) => {
1032                    let attr_name = name_value.path.get_ident().map(|id| id.to_string());
1033                    let attr_value = &name_value.lit;
1034
1035                    if let Some(attr_name) = attr_name {
1036                        if valid_attrs.contains(&attr_name.as_str()) {
1037                            if let Lit::Str(attr_value) = attr_value {
1038                                if security_attrs
1039                                    .insert(attr_name.clone(), attr_value.value())
1040                                    .is_some()
1041                                {
1042                                    emit_warning!(
1043                                        span,
1044                                        "Attribute {} defined multiple times.",
1045                                        attr_name
1046                                    );
1047                                }
1048                            } else {
1049                                emit_warning!(
1050                                    span,
1051                                    "Invalid value for named attribute: {}",
1052                                    attr_name
1053                                );
1054                            }
1055                        } else {
1056                            emit_warning!(span, invalid_attr_msg);
1057                        }
1058                    } else {
1059                        emit_error!(span, invalid_attr_msg);
1060                    }
1061                }
1062                // Read scopes attribute
1063                NestedMeta::Meta(Meta::List(list_attr)) => {
1064                    match list_attr
1065                        .path
1066                        .get_ident()
1067                        .map(|id| id.to_string())
1068                        .as_deref()
1069                    {
1070                        Some("scopes") => {
1071                            for nested in &list_attr.nested {
1072                                match nested {
1073                                    NestedMeta::Lit(Lit::Str(value)) => {
1074                                        scopes.push(value.value().to_string())
1075                                    }
1076                                    _ => emit_error!(
1077                                        nested.span().unwrap(),
1078                                        "Invalid list attribute value"
1079                                    ),
1080                                }
1081                            }
1082                        }
1083                        Some(path) => emit_error!(span, "Invalid list attribute: {}", path),
1084                        _ => emit_error!(span, "Invalid list attribute"),
1085                    }
1086                }
1087                _ => {
1088                    emit_error!(span, invalid_attr_msg);
1089                }
1090            }
1091        }
1092    }
1093
1094    let scopes_stream = scopes
1095        .iter()
1096        .fold(proc_macro2::TokenStream::new(), |mut stream, scope| {
1097            stream.extend(quote! {
1098                oauth2_scopes.insert(#scope.to_string(), #scope.to_string());
1099            });
1100            stream
1101        });
1102
1103    let (security_def, security_def_name) = match (
1104        security_attrs.get("type"),
1105        security_attrs.get("parent"),
1106    ) {
1107        (Some(type_), None) => {
1108            let alias = security_attrs.get("alias").unwrap_or(type_);
1109            let quoted_description = quote_option(security_attrs.get("description"));
1110            let quoted_name = quote_option(security_attrs.get("name"));
1111            let quoted_in = quote_option(security_attrs.get("in"));
1112            let quoted_flow = quote_option(security_attrs.get("flow"));
1113            let quoted_auth_url = quote_option(security_attrs.get("auth_url"));
1114            let quoted_token_url = quote_option(security_attrs.get("token_url"));
1115
1116            (
1117                Some(quote! {
1118                    Some(paperclip::v2::models::SecurityScheme {
1119                        type_: #type_.to_string(),
1120                        name: #quoted_name,
1121                        in_: #quoted_in,
1122                        flow: #quoted_flow,
1123                        auth_url: #quoted_auth_url,
1124                        token_url: #quoted_token_url,
1125                        scopes: std::collections::BTreeMap::new(),
1126                        description: #quoted_description,
1127                    })
1128                }),
1129                Some(quote!(Some(#alias.to_string()))),
1130            )
1131        }
1132        (None, Some(parent)) => {
1133            let parent_ident = Ident::new(parent, proc_macro2::Span::call_site());
1134            // Child of security definition (Scopes will be glued to parent definition).
1135            (
1136                Some(quote! {
1137                    let mut oauth2_scopes = std::collections::BTreeMap::new();
1138                    #scopes_stream
1139                    let mut scheme = #parent_ident::security_scheme()
1140                        .expect("empty schema. did you derive `Apiv2Security` for parent struct?");
1141                    scheme.scopes = oauth2_scopes;
1142                    Some(scheme)
1143                }),
1144                Some(quote!(<#parent_ident as paperclip::v2::schema::Apiv2Schema>::name())),
1145            )
1146        }
1147        (Some(_), Some(_)) => {
1148            emit_error!(
1149                item_ast.span().unwrap(),
1150                "Can't define new security type and use parent attribute together."
1151            );
1152            (None, None)
1153        }
1154        (None, None) => {
1155            emit_error!(
1156                item_ast.span().unwrap(),
1157                "Invalid attributes. Expected security type or parent defined."
1158            );
1159            (None, None)
1160        }
1161    };
1162
1163    let gen = if let (Some(def_block), Some(def_name)) = (security_def, security_def_name) {
1164        quote! {
1165            impl #impl_generics paperclip::v2::schema::Apiv2Schema for #name #ty_generics #where_clause {
1166                fn name() -> Option<String> {
1167                    #def_name
1168                }
1169
1170                fn security_scheme() -> Option<paperclip::v2::models::SecurityScheme> {
1171                    #def_block
1172                }
1173            }
1174
1175            #opt_impl
1176        }
1177    } else {
1178        quote! {}
1179    };
1180
1181    gen.into()
1182}
1183
1184/// Actual parser and emitter for `Apiv2Header` derive macro.
1185pub fn emit_v2_header(input: TokenStream) -> TokenStream {
1186    let item_ast = match crate::expect_struct_or_enum(input) {
1187        Ok(i) => i,
1188        Err(ts) => return ts,
1189    };
1190
1191    if let Some(empty) = check_empty_schema(&item_ast) {
1192        return empty;
1193    }
1194
1195    let name = &item_ast.ident;
1196    // Add `Apiv2Schema` bound for impl if the type is generic.
1197    let mut generics = item_ast.generics.clone();
1198    let bound = syn::parse2::<TraitBound>(quote!(paperclip::v2::schema::Apiv2Schema))
1199        .expect("expected to parse trait bound");
1200    generics.type_params_mut().for_each(|param| {
1201        param.bounds.push(bound.clone().into());
1202    });
1203
1204    let opt_impl = add_optional_impl(name, &generics);
1205    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
1206
1207    let mut header_definitions = vec![];
1208
1209    let valid_attrs = vec!["description", "name", "format", "maximum", "minimum"];
1210    let invalid_attr_msg = format!(
1211        "Invalid macro attribute. Should be named attribute {:?}",
1212        valid_attrs
1213    );
1214
1215    fn quote_format(format: &str) -> proc_macro2::TokenStream {
1216        match format {
1217            "int32" => quote! { Some(paperclip::v2::models::DataTypeFormat::Int32) },
1218            "int64" => quote! { Some(paperclip::v2::models::DataTypeFormat::Int64) },
1219            "float" => quote! { Some(paperclip::v2::models::DataTypeFormat::Float) },
1220            "double" => quote! { Some(paperclip::v2::models::DataTypeFormat::Double) },
1221            "byte" => quote! { Some(paperclip::v2::models::DataTypeFormat::Byte) },
1222            "binary" => quote! { Some(paperclip::v2::models::DataTypeFormat::Binary) },
1223            "date" => quote! { Some(paperclip::v2::models::DataTypeFormat::Date) },
1224            "datetime" | "date-time" => {
1225                quote! { Some(paperclip::v2::models::DataTypeFormat::DateTime) }
1226            }
1227            "password" => quote! { Some(paperclip::v2::models::DataTypeFormat::Password) },
1228            "url" => quote! { Some(paperclip::v2::models::DataTypeFormat::Url) },
1229            "uuid" => quote! { Some(paperclip::v2::models::DataTypeFormat::Uuid) },
1230            "ip" => quote! { Some(paperclip::v2::models::DataTypeFormat::Ip) },
1231            "ipv4" => quote! { Some(paperclip::v2::models::DataTypeFormat::IpV4) },
1232            "ipv6" => quote! { Some(paperclip::v2::models::DataTypeFormat::IpV6) },
1233            "other" => quote! { Some(paperclip::v2::models::DataTypeFormat::Other) },
1234            v => {
1235                emit_error!(
1236                    format.span().unwrap(),
1237                    format!("Invalid format attribute value. Got {}", v)
1238                );
1239                quote! { None }
1240            }
1241        }
1242    }
1243
1244    let struct_ast = match &item_ast.data {
1245        Data::Struct(struct_ast) => struct_ast,
1246        Data::Enum(_) | Data::Union(_) => {
1247            emit_error!(
1248                item_ast.span(),
1249                "Invalid data type. Apiv2Header should be defined on a struct"
1250            );
1251            return quote!().into();
1252        }
1253    };
1254
1255    if extract_openapi_attrs(&item_ast.attrs)
1256        .peekable()
1257        .peek()
1258        .is_some()
1259    {
1260        emit_error!(
1261            item_ast.span(),
1262            "Invalid openapi attribute. openapi attribute should be defined at struct fields level"
1263        );
1264        return quote!().into();
1265    }
1266
1267    for field in &struct_ast.fields {
1268        let mut parameter_attrs = HashMap::new();
1269        let field_name = &field.ident;
1270        let docs = extract_documentation(&field.attrs);
1271        let docs = docs.trim();
1272
1273        // Read header params from openapi attr.
1274        for nested in extract_openapi_attrs(&field.attrs) {
1275            for nested_attr in nested {
1276                let span = nested_attr.span().unwrap();
1277                match &nested_attr {
1278                    // Read bare attribute (support for skip attribute)
1279                    NestedMeta::Meta(Meta::Path(attr_path)) => {
1280                        if let Some(attr) = attr_path.get_ident() {
1281                            if *attr == "skip" {
1282                                parameter_attrs.insert("skip".to_owned(), "".to_owned());
1283                            }
1284                        }
1285                    }
1286                    // Read named attribute.
1287                    NestedMeta::Meta(Meta::NameValue(name_value)) => {
1288                        let attr_name = name_value.path.get_ident().map(|id| id.to_string());
1289                        let attr_value = &name_value.lit;
1290
1291                        if let Some(attr_name) = attr_name {
1292                            if valid_attrs.contains(&attr_name.as_str()) {
1293                                if let Some(value) = match attr_value {
1294                                    Lit::Str(attr_value) => Some(attr_value.value()),
1295                                    Lit::Float(x) => Some(x.to_string()),
1296                                    Lit::Int(x) => Some(x.to_string()),
1297                                    _ => {
1298                                        emit_warning!(
1299                                            span,
1300                                            "Invalid value for named attribute: {}",
1301                                            attr_name
1302                                        );
1303                                        None
1304                                    }
1305                                } {
1306                                    if parameter_attrs.insert(attr_name.clone(), value).is_some() {
1307                                        emit_warning!(
1308                                            span,
1309                                            "Attribute {} defined multiple times.",
1310                                            attr_name
1311                                        );
1312                                    }
1313                                }
1314                            } else {
1315                                emit_warning!(span, invalid_attr_msg);
1316                            }
1317                        } else {
1318                            emit_error!(span, invalid_attr_msg);
1319                        }
1320                    }
1321                    _ => {
1322                        emit_error!(span, invalid_attr_msg);
1323                    }
1324                }
1325            }
1326        }
1327
1328        if parameter_attrs.contains_key("skip") {
1329            continue;
1330        }
1331
1332        let docs = (!docs.is_empty()).then(|| docs.to_owned());
1333        let quoted_description = quote_option(parameter_attrs.get("description").or(docs.as_ref()));
1334        let name_string = field_name.as_ref().map(|name| name.to_string());
1335        let quoted_name = if let Some(name) = parameter_attrs.get("name").or(name_string.as_ref()) {
1336            name
1337        } else {
1338            emit_error!(
1339                field.span(),
1340                "Missing header name. Either add a name using the openapi attribute or use named struct parameter"
1341            );
1342            return quote!().into();
1343        };
1344
1345        let (quoted_type, quoted_format) = if let Some(ty_ref) = get_field_type(field) {
1346            (
1347                quote! { {
1348                    use paperclip::v2::schema::TypedData;
1349                    Some(#ty_ref::data_type())
1350                } },
1351                quote! { {
1352                    use paperclip::v2::schema::TypedData;
1353                    #ty_ref::format()
1354                } },
1355            )
1356        } else {
1357            (quote! { None }, quote! { None })
1358        };
1359
1360        let (quoted_type, quoted_format) = if let Some(format) = parameter_attrs.get("format") {
1361            let quoted_format = quote_format(format);
1362            let quoted_type = quote! { #quoted_format.map(|format| format.into()) };
1363            (quoted_type, quoted_format)
1364        } else {
1365            (quoted_type, quoted_format)
1366        };
1367
1368        let quoted_max = quote_option_str_f32(field, parameter_attrs.get("maximum"));
1369        let quoted_min = quote_option_str_f32(field, parameter_attrs.get("minimum"));
1370
1371        let def_block = quote! {
1372            paperclip::v2::models::Parameter::<paperclip::v2::models::DefaultSchemaRaw> {
1373                name: #quoted_name.to_owned(),
1374                in_: paperclip::v2::models::ParameterIn::Header,
1375                description: #quoted_description,
1376                data_type: #quoted_type,
1377                format: #quoted_format,
1378                maximum: #quoted_max,
1379                minimum: #quoted_min,
1380                required: Self::required(),
1381                ..Default::default()
1382            }
1383        };
1384
1385        header_definitions.push(def_block);
1386    }
1387
1388    let gen = quote! {
1389        impl #impl_generics paperclip::v2::schema::Apiv2Schema for #name #ty_generics #where_clause {
1390            fn header_parameter_schema() -> Vec<paperclip::v2::models::Parameter<paperclip::v2::models::DefaultSchemaRaw>> {
1391                vec![
1392                    #(#header_definitions),*
1393                ]
1394            }
1395        }
1396
1397        #opt_impl
1398    };
1399
1400    gen.into()
1401}
1402
1403fn quote_option(value: Option<&String>) -> proc_macro2::TokenStream {
1404    if let Some(value) = value {
1405        quote! { Some(#value.to_string()) }
1406    } else {
1407        quote! { None }
1408    }
1409}
1410fn quote_option_str_f32(field: &Field, value: Option<&String>) -> proc_macro2::TokenStream {
1411    if let Some(x) = value {
1412        let x: f32 = match x.parse() {
1413            Ok(x) => x,
1414            Err(error) => {
1415                emit_error!(field.span(), error.to_string());
1416                0.0
1417            }
1418        };
1419        quote! { Some(#x) }
1420    } else {
1421        quote! { None }
1422    }
1423}
1424
1425#[cfg(feature = "nightly")]
1426fn add_optional_impl(_: &Ident, _: &Generics) -> proc_macro2::TokenStream {
1427    // Empty impl for "nightly" feature because specialization helps us there.
1428    quote!()
1429}
1430
1431#[cfg(not(feature = "nightly"))]
1432fn add_optional_impl(name: &Ident, generics: &Generics) -> proc_macro2::TokenStream {
1433    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
1434    quote! {
1435        impl #impl_generics paperclip::actix::OperationModifier for #name #ty_generics #where_clause {}
1436    }
1437}
1438
1439fn get_field_type(field: &Field) -> Option<proc_macro2::TokenStream> {
1440    match field.ty {
1441        Type::Path(_) | Type::Reference(_) | Type::Array(_) => {
1442            Some(address_type_for_fn_call(&field.ty))
1443        }
1444        _ => {
1445            emit_warning!(
1446                field.ty.span().unwrap(),
1447                "unsupported field type will be ignored."
1448            );
1449            None
1450        }
1451    }
1452}
1453
1454/// Generates code for a tuple struct with fields.
1455fn handle_unnamed_field_struct(
1456    fields: &FieldsUnnamed,
1457    struct_attr: &[Attribute],
1458    props_gen: &mut proc_macro2::TokenStream,
1459) {
1460    if fields.unnamed.len() == 1 {
1461        let field = fields.unnamed.iter().next().unwrap();
1462
1463        if let Some(ty_ref) = get_field_type(field) {
1464            let docs = extract_documentation(struct_attr);
1465            let docs = docs.trim();
1466
1467            if SerdeSkip::exists(&field.attrs) {
1468                props_gen.extend(quote!({
1469                    let mut s: DefaultSchemaRaw = Default::default();
1470                    if !#docs.is_empty() {
1471                        s.description = Some(#docs.to_string());
1472                    }
1473                    schema = s;
1474                }));
1475            } else {
1476                props_gen.extend(quote!({
1477                    let mut s = #ty_ref::raw_schema();
1478                    if !#docs.is_empty() {
1479                        s.description = Some(#docs.to_string());
1480                    }
1481                    schema = s;
1482                }));
1483            }
1484        }
1485    } else {
1486        for (inner_field_id, field) in fields.unnamed.iter().enumerate() {
1487            if SerdeSkip::exists(&field.attrs) {
1488                continue;
1489            }
1490
1491            let ty_ref = match get_field_type(field) {
1492                Some(ty_ref) => ty_ref,
1493                None => continue,
1494            };
1495
1496            let docs = extract_documentation(&field.attrs);
1497            let docs = docs.trim();
1498
1499            let gen = if !SerdeFlatten::exists(&field.attrs) {
1500                // this is really not what we'd want to do because that's not how the
1501                // deserialized struct will be like, ideally we want an actual tuple
1502                // this type should therefore not be used for anything else than `Path`
1503                quote!({
1504                    let mut s = #ty_ref::raw_schema();
1505                    if !#docs.is_empty() {
1506                        s.description = Some(#docs.to_string());
1507                    }
1508                    schema.properties.insert(#inner_field_id.to_string(), s.into());
1509                    if #ty_ref::required() {
1510                        schema.required.insert(#inner_field_id.to_string());
1511                    }
1512                })
1513            } else {
1514                quote!({
1515                    let s = #ty_ref::raw_schema();
1516                    schema.properties.extend(s.properties);
1517
1518                    if #ty_ref::required() {
1519                        schema.required.extend(s.required);
1520                    }
1521                })
1522            };
1523
1524            props_gen.extend(gen);
1525        }
1526    }
1527}
1528
1529/// Checks for `api_v2_empty` attributes and removes them.
1530fn extract_openapi_attrs(
1531    field_attrs: &'_ [Attribute],
1532) -> impl Iterator<Item = Punctuated<syn::NestedMeta, syn::token::Comma>> + '_ {
1533    field_attrs.iter().filter_map(|a| match a.parse_meta() {
1534        Ok(Meta::List(list)) if list.path.is_ident(SCHEMA_MACRO_ATTR) => Some(list.nested),
1535        _ => None,
1536    })
1537}
1538
1539fn extract_deprecated(attrs: &[Attribute]) -> bool {
1540    attrs.iter().any(|a| match a.parse_meta() {
1541        Ok(Meta::Path(mp)) if mp.is_ident("deprecated") => true,
1542        Ok(Meta::List(mml)) => mml
1543            .path
1544            .segments
1545            .into_iter()
1546            .any(|p| p.ident == "deprecated"),
1547        _ => false,
1548    })
1549}
1550
1551/// Checks for `api_v2_empty` attributes and removes them.
1552fn extract_documentation(attrs: &[Attribute]) -> String {
1553    attrs
1554        .iter()
1555        .filter_map(|a| match a.parse_meta() {
1556            Ok(Meta::NameValue(mnv)) if mnv.path.is_ident("doc") => match &mnv.lit {
1557                Lit::Str(s) => Some(s.value()),
1558                _ => None,
1559            },
1560            _ => None,
1561        })
1562        .collect::<Vec<String>>()
1563        .join("\n")
1564}
1565
1566/// Checks if an empty schema has been requested and generate if needed.
1567fn check_empty_schema(item_ast: &DeriveInput) -> Option<TokenStream> {
1568    let needs_empty_schema = extract_openapi_attrs(&item_ast.attrs).any(|nested| {
1569        nested.len() == 1
1570            && match &nested[0] {
1571                NestedMeta::Meta(Meta::Path(path)) => path.is_ident("empty"),
1572                _ => false,
1573            }
1574    });
1575
1576    if needs_empty_schema {
1577        let name = &item_ast.ident;
1578        let generics = item_ast.generics.clone();
1579        let opt_impl = add_optional_impl(name, &generics);
1580        let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
1581        return Some(quote!(
1582            impl #impl_generics paperclip::v2::schema::Apiv2Schema for #name #ty_generics #where_clause {}
1583
1584            #opt_impl
1585        ).into());
1586    }
1587
1588    None
1589}
1590
1591/// Generates code for a struct with fields.
1592fn handle_field_struct(
1593    fields: &FieldsNamed,
1594    struct_attr: &[Attribute],
1595    serde: &SerdeProps,
1596    props_gen: &mut proc_macro2::TokenStream,
1597) {
1598    let docs = extract_documentation(struct_attr);
1599    let docs = docs.trim();
1600
1601    props_gen.extend(quote!({
1602        if !#docs.is_empty() {
1603            schema.description = Some(#docs.to_string());
1604        }
1605    }));
1606    for field in &fields.named {
1607        let mut field_name = field
1608            .ident
1609            .as_ref()
1610            .expect("missing field name?")
1611            .to_string();
1612
1613        //Strip r# prefix if any
1614        field_name = field_name
1615            .strip_prefix("r#")
1616            .map(|n| n.to_string())
1617            .unwrap_or(field_name);
1618
1619        if SerdeSkip::exists(&field.attrs) {
1620            continue;
1621        }
1622
1623        if let Some(renamed) = SerdeRename::from_field_attrs(&field.attrs) {
1624            field_name = renamed;
1625        } else if let Some(prop) = serde.rename {
1626            field_name = prop.rename(&field_name);
1627        }
1628
1629        let ty_ref = match get_field_type(field) {
1630            Some(ty_ref) => ty_ref,
1631            None => continue,
1632        };
1633
1634        let docs = extract_documentation(&field.attrs);
1635        let docs = docs.trim();
1636
1637        let example = if let Some(example) = extract_example(&field.attrs) {
1638            // allow to parse escaped json string or single str value
1639            quote!({
1640                s.example = paperclip::v2::serde_json::from_str::<paperclip::v2::serde_json::Value>(#example).ok().or_else(|| Some(#example.into()));
1641            })
1642        } else {
1643            quote!({})
1644        };
1645
1646        let max = if let Some(max) = extract_openapi_f32(&field.attrs, "maximum") {
1647            quote!({
1648                s.maximum = Some(#max);
1649            })
1650        } else {
1651            quote!({})
1652        };
1653        let min = if let Some(min) = extract_openapi_f32(&field.attrs, "minimum") {
1654            quote!({
1655                s.minimum = Some(#min);
1656            })
1657        } else {
1658            quote!({})
1659        };
1660
1661        let gen = if !SerdeFlatten::exists(&field.attrs) {
1662            quote!({
1663                let mut s = #ty_ref::raw_schema();
1664                if !#docs.is_empty() {
1665                    s.description = Some(#docs.to_string());
1666                }
1667                #example;
1668                #max;
1669                #min;
1670                schema.properties.insert(#field_name.into(), s.into());
1671
1672                if #ty_ref::required() {
1673                    schema.required.insert(#field_name.into());
1674                }
1675            })
1676        } else {
1677            quote!({
1678                let s = #ty_ref::raw_schema();
1679                schema.properties.extend(s.properties);
1680
1681                if #ty_ref::required() {
1682                    schema.required.extend(s.required);
1683                }
1684            })
1685        };
1686
1687        props_gen.extend(gen);
1688    }
1689}
1690
1691/// Generates code for an enum (if supported).
1692fn handle_enum(e: &DataEnum, serde: &SerdeProps, props_gen: &mut proc_macro2::TokenStream) {
1693    props_gen.extend(quote!(
1694        schema.data_type = Some(DataType::String);
1695    ));
1696
1697    for var in &e.variants {
1698        let mut name = var.ident.to_string();
1699        match &var.fields {
1700            Fields::Unit => (),
1701            Fields::Named(ref f) => {
1702                emit_warning!(
1703                    f.span().unwrap(),
1704                    "skipping enum variant with named fields in schema."
1705                );
1706                continue;
1707            }
1708            Fields::Unnamed(ref f) => {
1709                emit_warning!(f.span().unwrap(), "skipping tuple enum variant in schema.");
1710                continue;
1711            }
1712        }
1713
1714        if SerdeSkip::exists(&var.attrs) {
1715            continue;
1716        }
1717
1718        if let Some(renamed) = SerdeRename::from_field_attrs(&var.attrs) {
1719            name = renamed;
1720        } else if let Some(prop) = serde.rename {
1721            name = prop.rename(&name);
1722        }
1723
1724        props_gen.extend(quote!(
1725            schema.enum_.push(paperclip::v2::serde_json::json!(#name));
1726        ));
1727    }
1728}
1729
1730/// An associated function of a generic type, say, a vector cannot be called
1731/// like `Vec::foo` as it doesn't have a default type. We should instead call
1732/// `Vec::<T>::foo`. Something similar applies to `str`. This function takes
1733/// care of that special treatment.
1734fn address_type_for_fn_call(old_ty: &Type) -> proc_macro2::TokenStream {
1735    if matches!(old_ty, Type::Reference(_) | Type::Array(_)) {
1736        return quote!(<(#old_ty)>);
1737    }
1738
1739    let mut ty = old_ty.clone();
1740    if let Type::Path(ref mut p) = &mut ty {
1741        p.path.segments.pairs_mut().for_each(|mut pair| {
1742            let is_empty = pair.value().arguments.is_empty();
1743            let args = &mut pair.value_mut().arguments;
1744            match args {
1745                PathArguments::AngleBracketed(ref mut brack_args) if !is_empty => {
1746                    brack_args.colon2_token = Some(Token![::](proc_macro2::Span::call_site()));
1747                }
1748                _ => (),
1749            }
1750        });
1751    }
1752
1753    quote!(#ty)
1754}
1755
1756/* Serde attributes */
1757
1758/// Supported renaming options in serde (https://serde.rs/variant-attrs.html).
1759#[derive(Clone, Copy, Debug, Eq, PartialEq, EnumString)]
1760enum SerdeRename {
1761    #[strum(serialize = "lowercase")]
1762    Lower,
1763    #[strum(serialize = "UPPERCASE")]
1764    Upper,
1765    #[strum(serialize = "PascalCase")]
1766    Pascal,
1767    #[strum(serialize = "camelCase")]
1768    Camel,
1769    #[strum(serialize = "snake_case")]
1770    Snake,
1771    #[strum(serialize = "SCREAMING_SNAKE_CASE")]
1772    ScreamingSnake,
1773    #[strum(serialize = "kebab-case")]
1774    Kebab,
1775    #[strum(serialize = "SCREAMING-KEBAB-CASE")]
1776    ScreamingKebab,
1777}
1778
1779impl SerdeRename {
1780    /// Traverses the field attributes and returns the renamed value from the first matching
1781    /// `#[serde(rename = "...")]` pattern.
1782    fn from_field_attrs(field_attrs: &[Attribute]) -> Option<String> {
1783        for meta in field_attrs.iter().filter_map(|a| a.parse_meta().ok()) {
1784            let inner_meta = match meta {
1785                Meta::List(ref l)
1786                    if l.path
1787                        .segments
1788                        .last()
1789                        .map(|p| p.ident == "serde")
1790                        .unwrap_or(false) =>
1791                {
1792                    &l.nested
1793                }
1794                _ => continue,
1795            };
1796
1797            for meta in inner_meta {
1798                let rename = match meta {
1799                    NestedMeta::Meta(Meta::NameValue(ref v))
1800                        if v.path
1801                            .segments
1802                            .last()
1803                            .map(|p| p.ident == "rename")
1804                            .unwrap_or(false) =>
1805                    {
1806                        &v.lit
1807                    }
1808                    _ => continue,
1809                };
1810
1811                if let Lit::Str(ref s) = rename {
1812                    return Some(s.value());
1813                }
1814            }
1815        }
1816
1817        None
1818    }
1819
1820    /// Renames the given value using the current option.
1821    fn rename(self, name: &str) -> String {
1822        match self {
1823            SerdeRename::Lower => name.to_lowercase(),
1824            SerdeRename::Upper => name.to_uppercase(),
1825            SerdeRename::Pascal => name.to_pascal_case(),
1826            SerdeRename::Camel => name.to_lower_camel_case(),
1827            SerdeRename::Snake => name.to_snake_case(),
1828            SerdeRename::ScreamingSnake => name.to_shouty_snake_case(),
1829            SerdeRename::Kebab => name.to_kebab_case(),
1830            SerdeRename::ScreamingKebab => name.to_shouty_kebab_case(),
1831        }
1832    }
1833}
1834
1835/// Serde skip (https://serde.rs/variant-attrs.html)
1836/// Never serialize or deserialize this variant.
1837/// There are other variants available (skip_serializing,skip_deserializing) though it's not clear
1838/// how this should be handled since we use the same Schema for Ser/DeSer
1839struct SerdeSkip;
1840
1841impl SerdeSkip {
1842    /// Traverses the field attributes and returns whether the field should be skipped or not
1843    /// dependent on finding the `#[serde(skip]` attribute.
1844    fn exists(field_attrs: &[Attribute]) -> bool {
1845        for meta in field_attrs.iter().filter_map(|a| a.parse_meta().ok()) {
1846            let inner_meta = match meta {
1847                Meta::List(ref l)
1848                    if l.path
1849                        .segments
1850                        .last()
1851                        .map(|p| p.ident == "serde")
1852                        .unwrap_or(false) =>
1853                {
1854                    &l.nested
1855                }
1856                _ => continue,
1857            };
1858            for meta in inner_meta {
1859                if let NestedMeta::Meta(Meta::Path(path)) = meta {
1860                    if path.segments.iter().any(|s| s.ident == "skip") {
1861                        return true;
1862                    }
1863                }
1864            }
1865        }
1866
1867        false
1868    }
1869}
1870
1871#[derive(Clone, Debug, Default)]
1872struct SerdeProps {
1873    rename: Option<SerdeRename>,
1874}
1875
1876impl SerdeProps {
1877    /// Traverses the serde attributes in the given item attributes and returns
1878    /// the applicable properties.
1879    fn from_item_attrs(item_attrs: &[Attribute]) -> Self {
1880        let mut props = Self::default();
1881        for meta in item_attrs.iter().filter_map(|a| a.parse_meta().ok()) {
1882            let inner_meta = match meta {
1883                Meta::List(ref l)
1884                    if l.path
1885                        .segments
1886                        .last()
1887                        .map(|p| p.ident == "serde")
1888                        .unwrap_or(false) =>
1889                {
1890                    &l.nested
1891                }
1892                _ => continue,
1893            };
1894
1895            for meta in inner_meta {
1896                let global_rename = match meta {
1897                    NestedMeta::Meta(Meta::NameValue(ref v))
1898                        if v.path
1899                            .segments
1900                            .last()
1901                            .map(|p| p.ident == "rename_all")
1902                            .unwrap_or(false) =>
1903                    {
1904                        &v.lit
1905                    }
1906                    _ => continue,
1907                };
1908
1909                if let Lit::Str(ref s) = global_rename {
1910                    props.rename = s.value().parse().ok();
1911                }
1912            }
1913        }
1914
1915        props
1916    }
1917}
1918
1919/// Supported flattening of embedded struct (https://serde.rs/variant-attrs.html).
1920struct SerdeFlatten;
1921
1922impl SerdeFlatten {
1923    /// Traverses the field attributes and returns true if there is `#[serde(flatten)]`.
1924    fn exists(field_attrs: &[Attribute]) -> bool {
1925        for meta in field_attrs.iter().filter_map(|a| a.parse_meta().ok()) {
1926            let inner_meta = match meta {
1927                Meta::List(ref l)
1928                    if l.path
1929                        .segments
1930                        .last()
1931                        .map(|p| p.ident == "serde")
1932                        .unwrap_or(false) =>
1933                {
1934                    &l.nested
1935                }
1936                _ => continue,
1937            };
1938
1939            for meta in inner_meta {
1940                if let NestedMeta::Meta(Meta::Path(syn::Path { segments, .. })) = meta {
1941                    if segments.iter().any(|p| p.ident == "flatten") {
1942                        return true;
1943                    }
1944                }
1945            }
1946        }
1947
1948        false
1949    }
1950}
1951
1952macro_rules! doc_comment {
1953    ($x:expr; $($tt:tt)*) => {
1954        #[doc = $x]
1955        $($tt)*
1956    };
1957}
1958
1959#[cfg(feature = "actix")]
1960impl super::Method {
1961    fn handler_uri(attr: TokenStream) -> TokenStream {
1962        let attr = parse_macro_input!(attr as syn::AttributeArgs);
1963        attr.first().into_token_stream().into()
1964    }
1965    fn handler_name(item: TokenStream) -> syn::Result<syn::Ident> {
1966        let handler: ItemFn = syn::parse(item)?;
1967        Ok(handler.sig.ident)
1968    }
1969    pub(crate) fn generate(
1970        &self,
1971        attr: TokenStream,
1972        item: TokenStream,
1973    ) -> syn::Result<proc_macro2::TokenStream> {
1974        let uri: proc_macro2::TokenStream = Self::handler_uri(attr).into();
1975        let handler_name = Self::handler_name(item.clone())?;
1976        let handler_fn: proc_macro2::TokenStream = item.into();
1977        let method: proc_macro2::TokenStream = self.method().parse()?;
1978        let variant: proc_macro2::TokenStream = self.variant().parse()?;
1979        let handler_name_str = handler_name.to_string();
1980
1981        let uri = uri.to_string().replace('\"', ""); // The uri is a string lit, which contains quotes, remove them
1982
1983        let uri_fmt = if !uri.starts_with('/') {
1984            format!("/{}", uri)
1985        } else {
1986            uri
1987        };
1988
1989        Ok(quote! {
1990            #[allow(non_camel_case_types, missing_docs)]
1991            pub struct #handler_name;
1992
1993            impl #handler_name {
1994                fn resource() -> paperclip::actix::web::Resource {
1995                    #handler_fn
1996                    paperclip::actix::web::Resource::new(#uri_fmt)
1997                        .name(#handler_name_str)
1998                        .guard(actix_web::guard::#variant())
1999                        .route(paperclip::actix::web::#method().to(#handler_name))
2000                }
2001            }
2002
2003            impl actix_web::dev::HttpServiceFactory for #handler_name {
2004                fn register(self, config: &mut actix_web::dev::AppService) {
2005                    Self::resource().register(config);
2006                }
2007            }
2008
2009            impl paperclip::actix::Mountable for #handler_name {
2010                fn path(&self) -> &str {
2011                    #uri_fmt
2012                }
2013
2014                fn operations(
2015                    &mut self,
2016                ) -> std::collections::BTreeMap<
2017                    paperclip::v2::models::HttpMethod,
2018                    paperclip::v2::models::DefaultOperationRaw,
2019                > {
2020                    Self::resource().operations()
2021                }
2022
2023                fn definitions(
2024                    &mut self,
2025                ) -> std::collections::BTreeMap<
2026                    String,
2027                    paperclip::v2::models::DefaultSchemaRaw,
2028                > {
2029                    Self::resource().definitions()
2030                }
2031
2032                fn security_definitions(
2033                    &mut self,
2034                ) -> std::collections::BTreeMap<String, paperclip::v2::models::SecurityScheme>
2035                {
2036                    Self::resource().security_definitions()
2037                }
2038            }
2039        })
2040    }
2041}
2042
2043macro_rules! rest_methods {
2044    (
2045        $($variant:ident, $method:ident, )+
2046    ) => {
2047        /// All available Rest methods
2048        #[derive(Debug, PartialEq, Eq, Hash)]
2049        pub(crate) enum Method {
2050            $(
2051                $variant,
2052            )+
2053        }
2054
2055        impl Method {
2056            fn method(&self) -> &'static str {
2057                match self {
2058                    $(Self::$variant => stringify!($method),)+
2059                }
2060            }
2061            fn variant(&self) -> &'static str {
2062                match self {
2063                    $(Self::$variant => stringify!($variant),)+
2064                }
2065            }
2066        }
2067
2068        $(doc_comment! {
2069            concat!("
2070Creates route handler with `paperclip::actix::web::Resource", "`.
2071In order to control the output type and status codes the return value/response must implement the
2072trait actix_web::Responder.
2073
2074# Syntax
2075```text
2076#[", stringify!($method), r#"("path"[, attributes])]
2077```
2078
2079# Attributes
2080- `"path"` - Raw literal string with path for which to register handler.
2081
2082# Example
2083
2084/// use paperclip::actix::web::Json;
2085/// use paperclip_macros::"#, stringify!($method), ";
2086/// #[", stringify!($method), r#"("/")]
2087/// async fn example() {
2088/// }
2089"#);
2090            #[cfg(feature = "actix")]
2091            #[proc_macro_attribute]
2092            pub fn $method(attr: TokenStream, item: TokenStream) -> TokenStream {
2093                match Method::$variant.generate(attr, item) {
2094                    Ok(v) => v.into(),
2095                    Err(e) => e.to_compile_error().into(),
2096                }
2097            }
2098        })+
2099    };
2100}