utoipa_gen/
openapi.rs

1use proc_macro2::Ident;
2use syn::{
3    parenthesized,
4    parse::{Parse, ParseStream},
5    punctuated::Punctuated,
6    spanned::Spanned,
7    token::{And, Comma},
8    Attribute, Error, ExprPath, LitStr, Token, TypePath,
9};
10
11use proc_macro2::TokenStream;
12use quote::{format_ident, quote, quote_spanned, ToTokens};
13
14use crate::{
15    parse_utils, path::PATH_STRUCT_PREFIX, security_requirement::SecurityRequirementAttr, Array,
16    ExternalDocs, ResultExt,
17};
18
19use self::info::Info;
20
21mod info;
22
23#[derive(Default)]
24#[cfg_attr(feature = "debug", derive(Debug))]
25pub struct OpenApiAttr<'o> {
26    info: Option<Info<'o>>,
27    paths: Punctuated<ExprPath, Comma>,
28    components: Components,
29    modifiers: Punctuated<Modifier, Comma>,
30    security: Option<Array<'static, SecurityRequirementAttr>>,
31    tags: Option<Array<'static, Tag>>,
32    external_docs: Option<ExternalDocs>,
33    servers: Punctuated<Server, Comma>,
34}
35
36impl<'o> OpenApiAttr<'o> {
37    fn merge(mut self, other: OpenApiAttr<'o>) -> Self {
38        if other.info.is_some() {
39            self.info = other.info;
40        }
41        if !other.paths.is_empty() {
42            self.paths = other.paths;
43        }
44        if !other.components.schemas.is_empty() {
45            self.components.schemas = other.components.schemas;
46        }
47        if !other.components.responses.is_empty() {
48            self.components.responses = other.components.responses;
49        }
50        if other.security.is_some() {
51            self.security = other.security;
52        }
53        if other.tags.is_some() {
54            self.tags = other.tags;
55        }
56        if other.external_docs.is_some() {
57            self.external_docs = other.external_docs;
58        }
59        if !other.servers.is_empty() {
60            self.servers = other.servers;
61        }
62
63        self
64    }
65}
66
67pub fn parse_openapi_attrs(attrs: &[Attribute]) -> Option<OpenApiAttr> {
68    attrs
69        .iter()
70        .filter(|attribute| attribute.path().is_ident("openapi"))
71        .map(|attribute| attribute.parse_args::<OpenApiAttr>().unwrap_or_abort())
72        .reduce(|acc, item| acc.merge(item))
73}
74
75impl Parse for OpenApiAttr<'_> {
76    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
77        const EXPECTED_ATTRIBUTE: &str =
78            "unexpected attribute, expected any of: handlers, components, modifiers, security, tags, external_docs, servers";
79        let mut openapi = OpenApiAttr::default();
80
81        while !input.is_empty() {
82            let ident = input.parse::<Ident>().map_err(|error| {
83                Error::new(error.span(), format!("{EXPECTED_ATTRIBUTE}, {error}"))
84            })?;
85            let attribute = &*ident.to_string();
86
87            match attribute {
88                "info" => {
89                    let info_stream;
90                    parenthesized!(info_stream in input);
91                    openapi.info = Some(info_stream.parse()?)
92                }
93                "paths" => {
94                    openapi.paths = parse_utils::parse_punctuated_within_parenthesis(input)?;
95                }
96                "components" => {
97                    openapi.components = input.parse()?;
98                }
99                "modifiers" => {
100                    openapi.modifiers = parse_utils::parse_punctuated_within_parenthesis(input)?;
101                }
102                "security" => {
103                    let security;
104                    parenthesized!(security in input);
105                    openapi.security = Some(parse_utils::parse_groups(&security)?)
106                }
107                "tags" => {
108                    let tags;
109                    parenthesized!(tags in input);
110                    openapi.tags = Some(parse_utils::parse_groups(&tags)?);
111                }
112                "external_docs" => {
113                    let external_docs;
114                    parenthesized!(external_docs in input);
115                    openapi.external_docs = Some(external_docs.parse()?);
116                }
117                "servers" => {
118                    openapi.servers = parse_utils::parse_punctuated_within_parenthesis(input)?;
119                }
120                _ => {
121                    return Err(Error::new(ident.span(), EXPECTED_ATTRIBUTE));
122                }
123            }
124
125            if !input.is_empty() {
126                input.parse::<Token![,]>()?;
127            }
128        }
129
130        Ok(openapi)
131    }
132}
133
134#[cfg_attr(feature = "debug", derive(Debug))]
135struct Schema(TypePath);
136
137impl Parse for Schema {
138    fn parse(input: ParseStream) -> syn::Result<Self> {
139        input.parse().map(Self)
140    }
141}
142
143#[cfg_attr(feature = "debug", derive(Debug))]
144struct Response(TypePath);
145
146impl Parse for Response {
147    fn parse(input: ParseStream) -> syn::Result<Self> {
148        input.parse().map(Self)
149    }
150}
151
152#[cfg_attr(feature = "debug", derive(Debug))]
153struct Modifier {
154    and: And,
155    ident: Ident,
156}
157
158impl ToTokens for Modifier {
159    fn to_tokens(&self, tokens: &mut TokenStream) {
160        let and = &self.and;
161        let ident = &self.ident;
162        tokens.extend(quote! {
163            #and #ident
164        })
165    }
166}
167
168impl Parse for Modifier {
169    fn parse(input: ParseStream) -> syn::Result<Self> {
170        Ok(Self {
171            and: input.parse()?,
172            ident: input.parse()?,
173        })
174    }
175}
176
177#[derive(Default)]
178#[cfg_attr(feature = "debug", derive(Debug))]
179struct Tag {
180    name: String,
181    description: Option<String>,
182    external_docs: Option<ExternalDocs>,
183}
184
185impl Parse for Tag {
186    fn parse(input: ParseStream) -> syn::Result<Self> {
187        const EXPECTED_ATTRIBUTE: &str =
188            "unexpected token, expected any of: name, description, external_docs";
189
190        let mut tag = Tag::default();
191
192        while !input.is_empty() {
193            let ident = input.parse::<Ident>().map_err(|error| {
194                syn::Error::new(error.span(), format!("{EXPECTED_ATTRIBUTE}, {error}"))
195            })?;
196            let attribute_name = &*ident.to_string();
197
198            match attribute_name {
199                "name" => tag.name = parse_utils::parse_next_literal_str(input)?,
200                "description" => {
201                    tag.description = Some(parse_utils::parse_next_literal_str(input)?)
202                }
203                "external_docs" => {
204                    let content;
205                    parenthesized!(content in input);
206                    tag.external_docs = Some(content.parse::<ExternalDocs>()?);
207                }
208                _ => return Err(syn::Error::new(ident.span(), EXPECTED_ATTRIBUTE)),
209            }
210
211            if !input.is_empty() {
212                input.parse::<Token![,]>()?;
213            }
214        }
215
216        Ok(tag)
217    }
218}
219
220impl ToTokens for Tag {
221    fn to_tokens(&self, tokens: &mut TokenStream) {
222        let name = &self.name;
223        tokens.extend(quote! {
224            utoipa::openapi::tag::TagBuilder::new().name(#name)
225        });
226
227        if let Some(ref description) = self.description {
228            tokens.extend(quote! {
229                .description(Some(#description))
230            });
231        }
232
233        if let Some(ref external_docs) = self.external_docs {
234            tokens.extend(quote! {
235                .external_docs(Some(#external_docs))
236            });
237        }
238
239        tokens.extend(quote! { .build() })
240    }
241}
242
243// (url = "http:://url", description = "description", variables(...))
244#[derive(Default)]
245#[cfg_attr(feature = "debug", derive(Debug))]
246struct Server {
247    url: String,
248    description: Option<String>,
249    variables: Punctuated<ServerVariable, Comma>,
250}
251
252impl Parse for Server {
253    fn parse(input: ParseStream) -> syn::Result<Self> {
254        let server_stream;
255        parenthesized!(server_stream in input);
256        let mut server = Server::default();
257        while !server_stream.is_empty() {
258            let ident = server_stream.parse::<Ident>()?;
259            let attribute_name = &*ident.to_string();
260
261            match attribute_name {
262                "url" => {
263                    server.url = parse_utils::parse_next(&server_stream, || server_stream.parse::<LitStr>())?.value()
264                }
265                "description" => {
266                    server.description =
267                        Some(parse_utils::parse_next(&server_stream, || server_stream.parse::<LitStr>())?.value())
268                }
269                "variables" => {
270                    server.variables = parse_utils::parse_punctuated_within_parenthesis(&server_stream)?
271                }
272                _ => {
273                    return Err(Error::new(ident.span(), format!("unexpected attribute: {attribute_name}, expected one of: url, description, variables")))
274                }
275            }
276
277            if !server_stream.is_empty() {
278                server_stream.parse::<Comma>()?;
279            }
280        }
281
282        Ok(server)
283    }
284}
285
286impl ToTokens for Server {
287    fn to_tokens(&self, tokens: &mut TokenStream) {
288        let url = &self.url;
289        let description = &self
290            .description
291            .as_ref()
292            .map(|description| quote! { .description(Some(#description)) });
293
294        let parameters = self
295            .variables
296            .iter()
297            .map(|variable| {
298                let name = &variable.name;
299                let default_value = &variable.default;
300                let description = &variable
301                    .description
302                    .as_ref()
303                    .map(|description| quote! { .description(Some(#description)) });
304                let enum_values = &variable.enum_values.as_ref().map(|enum_values| {
305                    let enum_values = enum_values.iter().collect::<Array<&LitStr>>();
306
307                    quote! { .enum_values(Some(#enum_values)) }
308                });
309
310                quote! {
311                    .parameter(#name, utoipa::openapi::server::ServerVariableBuilder::new()
312                        .default_value(#default_value)
313                        #description
314                        #enum_values
315                    )
316                }
317            })
318            .collect::<TokenStream>();
319
320        tokens.extend(quote! {
321            utoipa::openapi::server::ServerBuilder::new()
322                .url(#url)
323                #description
324                #parameters
325                .build()
326        })
327    }
328}
329
330// ("username" = (default = "demo", description = "This is default username for the API")),
331// ("port" = (enum_values = (8080, 5000, 4545)))
332#[derive(Default)]
333#[cfg_attr(feature = "debug", derive(Debug))]
334struct ServerVariable {
335    name: String,
336    default: String,
337    description: Option<String>,
338    enum_values: Option<Punctuated<LitStr, Comma>>,
339}
340
341impl Parse for ServerVariable {
342    fn parse(input: ParseStream) -> syn::Result<Self> {
343        let variable_stream;
344        parenthesized!(variable_stream in input);
345        let mut server_variable = ServerVariable {
346            name: variable_stream.parse::<LitStr>()?.value(),
347            ..ServerVariable::default()
348        };
349
350        variable_stream.parse::<Token![=]>()?;
351        let content;
352        parenthesized!(content in variable_stream);
353
354        while !content.is_empty() {
355            let ident = content.parse::<Ident>()?;
356            let attribute_name = &*ident.to_string();
357
358            match attribute_name {
359                "default" => {
360                    server_variable.default =
361                        parse_utils::parse_next(&content, || content.parse::<LitStr>())?.value()
362                }
363                "description" => {
364                    server_variable.description =
365                        Some(parse_utils::parse_next(&content, || content.parse::<LitStr>())?.value())
366                }
367                "enum_values" => {
368                    server_variable.enum_values =
369                        Some(parse_utils::parse_punctuated_within_parenthesis(&content)?)
370                }
371                _ => {
372                    return Err(Error::new(ident.span(), format!( "unexpected attribute: {attribute_name}, expected one of: default, description, enum_values")))
373                }
374            }
375
376            if !content.is_empty() {
377                content.parse::<Comma>()?;
378            }
379        }
380
381        Ok(server_variable)
382    }
383}
384
385pub(crate) struct OpenApi<'o>(pub OpenApiAttr<'o>, pub Ident);
386
387impl ToTokens for OpenApi<'_> {
388    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
389        let OpenApi(attributes, ident) = self;
390
391        let info = info::impl_info(attributes.info.clone());
392
393        let components_builder_stream = attributes.components.to_token_stream();
394
395        let components = if !components_builder_stream.is_empty() {
396            Some(quote! { .components(Some(#components_builder_stream)) })
397        } else {
398            None
399        };
400
401        let modifiers = &attributes.modifiers;
402        let modifiers_len = modifiers.len();
403
404        let path_items = impl_paths(&attributes.paths);
405
406        let securities = attributes.security.as_ref().map(|securities| {
407            quote! {
408                .security(Some(#securities))
409            }
410        });
411        let tags = attributes.tags.as_ref().map(|tags| {
412            quote! {
413                .tags(Some(#tags))
414            }
415        });
416        let external_docs = attributes.external_docs.as_ref().map(|external_docs| {
417            quote! {
418                .external_docs(Some(#external_docs))
419            }
420        });
421        let servers = if !attributes.servers.is_empty() {
422            let servers = attributes.servers.iter().collect::<Array<&Server>>();
423            Some(quote! { .servers(Some(#servers)) })
424        } else {
425            None
426        };
427
428        tokens.extend(quote! {
429            impl utoipa::OpenApi for #ident {
430                fn openapi() -> utoipa::openapi::OpenApi {
431                    use utoipa::{ToSchema, Path};
432                    let mut openapi = utoipa::openapi::OpenApiBuilder::new()
433                        .info(#info)
434                        .paths(#path_items)
435                        #components
436                        #securities
437                        #tags
438                        #servers
439                        #external_docs
440                        .build();
441
442                    let _mods: [&dyn utoipa::Modify; #modifiers_len] = [#modifiers];
443                    _mods.iter().for_each(|modifier| modifier.modify(&mut openapi));
444
445                    openapi
446                }
447            }
448        });
449    }
450}
451
452#[derive(Default)]
453#[cfg_attr(feature = "debug", derive(Debug))]
454struct Components {
455    schemas: Vec<Schema>,
456    responses: Vec<Response>,
457}
458
459impl Parse for Components {
460    fn parse(input: ParseStream) -> syn::Result<Self> {
461        let content;
462        parenthesized!(content in input);
463        const EXPECTED_ATTRIBUTE: &str =
464            "unexpected attribute. expected one of: schemas, responses";
465
466        let mut schemas: Vec<Schema> = Vec::new();
467        let mut responses: Vec<Response> = Vec::new();
468
469        while !content.is_empty() {
470            let ident = content.parse::<Ident>().map_err(|error| {
471                Error::new(error.span(), format!("{EXPECTED_ATTRIBUTE}, {error}"))
472            })?;
473            let attribute = &*ident.to_string();
474
475            match attribute {
476                "schemas" => schemas.append(
477                    &mut parse_utils::parse_punctuated_within_parenthesis(&content)?
478                        .into_iter()
479                        .collect(),
480                ),
481                "responses" => responses.append(
482                    &mut parse_utils::parse_punctuated_within_parenthesis(&content)?
483                        .into_iter()
484                        .collect(),
485                ),
486                _ => return Err(syn::Error::new(ident.span(), EXPECTED_ATTRIBUTE)),
487            }
488
489            if !content.is_empty() {
490                content.parse::<Token![,]>()?;
491            }
492        }
493
494        Ok(Self { schemas, responses })
495    }
496}
497
498impl ToTokens for Components {
499    fn to_tokens(&self, tokens: &mut TokenStream) {
500        if self.schemas.is_empty() && self.responses.is_empty() {
501            return;
502        }
503
504        let builder_tokens = self.schemas.iter().fold(
505            quote! { utoipa::openapi::ComponentsBuilder::new() },
506            |mut tokens, schema| {
507                let Schema(path) = schema;
508
509                tokens.extend(quote_spanned!(path.span()=>
510                     .schema_from::<#path>()
511                ));
512
513                tokens
514            },
515        );
516
517        let builder_tokens =
518            self.responses
519                .iter()
520                .fold(builder_tokens, |mut builder_tokens, responses| {
521                    let Response(path) = responses;
522
523                    builder_tokens.extend(quote_spanned! {path.span() =>
524                        .response_from::<#path>()
525                    });
526                    builder_tokens
527                });
528
529        tokens.extend(quote! { #builder_tokens.build() });
530    }
531}
532
533fn impl_paths(handler_paths: &Punctuated<ExprPath, Comma>) -> TokenStream {
534    handler_paths.iter().fold(
535        quote! { utoipa::openapi::path::PathsBuilder::new() },
536        |mut paths, handler| {
537            let segments = handler.path.segments.iter().collect::<Vec<_>>();
538            let handler_fn_name = &*segments.last().unwrap().ident.to_string();
539
540            let tag = &*segments
541                .iter()
542                .take(segments.len() - 1)
543                .map(|part| part.ident.to_string())
544                .collect::<Vec<_>>()
545                .join("::");
546
547            let handler_ident = format_ident!("{}{}", PATH_STRUCT_PREFIX, handler_fn_name);
548            let handler_ident_name = &*handler_ident.to_string();
549
550            let usage = syn::parse_str::<ExprPath>(
551                &vec![
552                    if tag.is_empty() { None } else { Some(tag) },
553                    Some(handler_ident_name),
554                ]
555                .into_iter()
556                .flatten()
557                .collect::<Vec<_>>()
558                .join("::"),
559            )
560            .unwrap();
561
562            paths.extend(quote! {
563                .path(#usage::path(), #usage::path_item(Some(#tag)))
564            });
565
566            paths
567        },
568    )
569}