utoipa_gen/
openapi.rs

1use std::borrow::Cow;
2
3use proc_macro2::Ident;
4use syn::{
5    bracketed, parenthesized,
6    parse::{Parse, ParseStream},
7    punctuated::Punctuated,
8    spanned::Spanned,
9    token::{And, Comma},
10    Attribute, Error, ExprPath, LitStr, Token, TypePath,
11};
12
13use proc_macro2::TokenStream;
14use quote::{format_ident, quote, quote_spanned, ToTokens};
15
16use crate::{
17    component::{features::Feature, ComponentSchema, Container, TypeTree},
18    parse_utils,
19    security_requirement::SecurityRequirementsAttr,
20    Array, Diagnostics, ExternalDocs, ToTokensDiagnostics,
21};
22use crate::{path, OptionExt};
23
24use self::info::Info;
25
26mod info;
27
28#[derive(Default)]
29#[cfg_attr(feature = "debug", derive(Debug))]
30pub struct OpenApiAttr<'o> {
31    info: Option<Info<'o>>,
32    paths: Punctuated<ExprPath, Comma>,
33    components: Components,
34    modifiers: Punctuated<Modifier, Comma>,
35    security: Option<Array<'static, SecurityRequirementsAttr>>,
36    tags: Option<Array<'static, Tag>>,
37    external_docs: Option<ExternalDocs>,
38    servers: Punctuated<Server, Comma>,
39    nested: Vec<NestOpenApi>,
40}
41
42impl<'o> OpenApiAttr<'o> {
43    fn merge(mut self, other: OpenApiAttr<'o>) -> Self {
44        if other.info.is_some() {
45            self.info = other.info;
46        }
47        if !other.paths.is_empty() {
48            self.paths = other.paths;
49        }
50        if !other.components.schemas.is_empty() {
51            self.components.schemas = other.components.schemas;
52        }
53        if !other.components.responses.is_empty() {
54            self.components.responses = other.components.responses;
55        }
56        if other.security.is_some() {
57            self.security = other.security;
58        }
59        if other.tags.is_some() {
60            self.tags = other.tags;
61        }
62        if other.external_docs.is_some() {
63            self.external_docs = other.external_docs;
64        }
65        if !other.servers.is_empty() {
66            self.servers = other.servers;
67        }
68
69        self
70    }
71}
72
73pub fn parse_openapi_attrs(attrs: &[Attribute]) -> Result<Option<OpenApiAttr>, Error> {
74    attrs
75        .iter()
76        .filter(|attribute| attribute.path().is_ident("openapi"))
77        .map(|attribute| attribute.parse_args::<OpenApiAttr>())
78        .collect::<Result<Vec<_>, _>>()
79        .map(|attrs| attrs.into_iter().reduce(|acc, item| acc.merge(item)))
80}
81
82impl Parse for OpenApiAttr<'_> {
83    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
84        const EXPECTED_ATTRIBUTE: &str =
85            "unexpected attribute, expected any of: handlers, components, modifiers, security, tags, external_docs, servers, nest";
86        let mut openapi = OpenApiAttr::default();
87
88        while !input.is_empty() {
89            let ident = input.parse::<Ident>().map_err(|error| {
90                Error::new(error.span(), format!("{EXPECTED_ATTRIBUTE}, {error}"))
91            })?;
92            let attribute = &*ident.to_string();
93
94            match attribute {
95                "info" => {
96                    let info_stream;
97                    parenthesized!(info_stream in input);
98                    openapi.info = Some(info_stream.parse()?)
99                }
100                "paths" => {
101                    openapi.paths = parse_utils::parse_comma_separated_within_parenthesis(input)?;
102                }
103                "components" => {
104                    openapi.components = input.parse()?;
105                }
106                "modifiers" => {
107                    openapi.modifiers =
108                        parse_utils::parse_comma_separated_within_parenthesis(input)?;
109                }
110                "security" => {
111                    let security;
112                    parenthesized!(security in input);
113                    openapi.security = Some(parse_utils::parse_groups_collect(&security)?)
114                }
115                "tags" => {
116                    let tags;
117                    parenthesized!(tags in input);
118                    openapi.tags = Some(parse_utils::parse_groups_collect(&tags)?);
119                }
120                "external_docs" => {
121                    let external_docs;
122                    parenthesized!(external_docs in input);
123                    openapi.external_docs = Some(external_docs.parse()?);
124                }
125                "servers" => {
126                    openapi.servers = parse_utils::parse_comma_separated_within_parenthesis(input)?;
127                }
128                "nest" => {
129                    let nest;
130                    parenthesized!(nest in input);
131                    openapi.nested = parse_utils::parse_groups_collect(&nest)?;
132                }
133                _ => {
134                    return Err(Error::new(ident.span(), EXPECTED_ATTRIBUTE));
135                }
136            }
137
138            if !input.is_empty() {
139                input.parse::<Token![,]>()?;
140            }
141        }
142
143        Ok(openapi)
144    }
145}
146
147#[cfg_attr(feature = "debug", derive(Debug))]
148struct Schema(TypePath);
149
150impl Schema {
151    fn get_component(&self) -> Result<ComponentSchema, Diagnostics> {
152        let ty = syn::Type::Path(self.0.clone());
153        let type_tree = TypeTree::from_type(&ty)?;
154        let generics = type_tree.get_path_generics()?;
155
156        let container = Container {
157            generics: &generics,
158        };
159        let component_schema = ComponentSchema::new(crate::component::ComponentSchemaProps {
160            container: &container,
161            type_tree: &type_tree,
162            features: vec![Feature::Inline(true.into())],
163            description: None,
164        })?;
165
166        Ok(component_schema)
167    }
168}
169
170impl Parse for Schema {
171    fn parse(input: ParseStream) -> syn::Result<Self> {
172        input.parse().map(Self)
173    }
174}
175
176#[cfg_attr(feature = "debug", derive(Debug))]
177struct Response(TypePath);
178
179impl Parse for Response {
180    fn parse(input: ParseStream) -> syn::Result<Self> {
181        input.parse().map(Self)
182    }
183}
184
185#[cfg_attr(feature = "debug", derive(Debug))]
186struct Modifier {
187    and: And,
188    ident: Ident,
189}
190
191impl ToTokens for Modifier {
192    fn to_tokens(&self, tokens: &mut TokenStream) {
193        let and = &self.and;
194        let ident = &self.ident;
195        tokens.extend(quote! {
196            #and #ident
197        })
198    }
199}
200
201impl Parse for Modifier {
202    fn parse(input: ParseStream) -> syn::Result<Self> {
203        Ok(Self {
204            and: input.parse()?,
205            ident: input.parse()?,
206        })
207    }
208}
209
210#[derive(Default)]
211#[cfg_attr(feature = "debug", derive(Debug))]
212struct Tag {
213    name: parse_utils::LitStrOrExpr,
214    description: Option<parse_utils::LitStrOrExpr>,
215    external_docs: Option<ExternalDocs>,
216}
217
218impl Parse for Tag {
219    fn parse(input: ParseStream) -> syn::Result<Self> {
220        const EXPECTED_ATTRIBUTE: &str =
221            "unexpected token, expected any of: name, description, external_docs";
222
223        let mut tag = Tag::default();
224
225        while !input.is_empty() {
226            let ident = input.parse::<Ident>().map_err(|error| {
227                syn::Error::new(error.span(), format!("{EXPECTED_ATTRIBUTE}, {error}"))
228            })?;
229            let attribute_name = &*ident.to_string();
230
231            match attribute_name {
232                "name" => tag.name = parse_utils::parse_next_literal_str_or_expr(input)?,
233                "description" => {
234                    tag.description = Some(parse_utils::parse_next_literal_str_or_expr(input)?)
235                }
236                "external_docs" => {
237                    let content;
238                    parenthesized!(content in input);
239                    tag.external_docs = Some(content.parse::<ExternalDocs>()?);
240                }
241                _ => return Err(syn::Error::new(ident.span(), EXPECTED_ATTRIBUTE)),
242            }
243
244            if !input.is_empty() {
245                input.parse::<Token![,]>()?;
246            }
247        }
248
249        Ok(tag)
250    }
251}
252
253impl ToTokens for Tag {
254    fn to_tokens(&self, tokens: &mut TokenStream) {
255        let name = &self.name;
256        tokens.extend(quote! {
257            utoipa::openapi::tag::TagBuilder::new().name(#name)
258        });
259
260        if let Some(ref description) = self.description {
261            tokens.extend(quote! {
262                .description(Some(#description))
263            });
264        }
265
266        if let Some(ref external_docs) = self.external_docs {
267            tokens.extend(quote! {
268                .external_docs(Some(#external_docs))
269            });
270        }
271
272        tokens.extend(quote! { .build() })
273    }
274}
275
276// (url = "http:://url", description = "description", variables(...))
277#[derive(Default)]
278#[cfg_attr(feature = "debug", derive(Debug))]
279pub struct Server {
280    url: String,
281    description: Option<String>,
282    variables: Punctuated<ServerVariable, Comma>,
283}
284
285impl Parse for Server {
286    fn parse(input: ParseStream) -> syn::Result<Self> {
287        let server_stream;
288        parenthesized!(server_stream in input);
289        let mut server = Server::default();
290        while !server_stream.is_empty() {
291            let ident = server_stream.parse::<Ident>()?;
292            let attribute_name = &*ident.to_string();
293
294            match attribute_name {
295                "url" => {
296                    server.url = parse_utils::parse_next(&server_stream, || server_stream.parse::<LitStr>())?.value()
297                }
298                "description" => {
299                    server.description =
300                        Some(parse_utils::parse_next(&server_stream, || server_stream.parse::<LitStr>())?.value())
301                }
302                "variables" => {
303                    server.variables = parse_utils::parse_comma_separated_within_parenthesis(&server_stream)?
304                }
305                _ => {
306                    return Err(Error::new(ident.span(), format!("unexpected attribute: {attribute_name}, expected one of: url, description, variables")))
307                }
308            }
309
310            if !server_stream.is_empty() {
311                server_stream.parse::<Comma>()?;
312            }
313        }
314
315        Ok(server)
316    }
317}
318
319impl ToTokens for Server {
320    fn to_tokens(&self, tokens: &mut TokenStream) {
321        let url = &self.url;
322        let description = &self
323            .description
324            .as_ref()
325            .map(|description| quote! { .description(Some(#description)) });
326
327        let parameters = self
328            .variables
329            .iter()
330            .map(|variable| {
331                let name = &variable.name;
332                let default_value = &variable.default;
333                let description = &variable
334                    .description
335                    .as_ref()
336                    .map(|description| quote! { .description(Some(#description)) });
337                let enum_values = &variable.enum_values.as_ref().map(|enum_values| {
338                    let enum_values = enum_values.iter().collect::<Array<&LitStr>>();
339
340                    quote! { .enum_values(Some(#enum_values)) }
341                });
342
343                quote! {
344                    .parameter(#name, utoipa::openapi::server::ServerVariableBuilder::new()
345                        .default_value(#default_value)
346                        #description
347                        #enum_values
348                    )
349                }
350            })
351            .collect::<TokenStream>();
352
353        tokens.extend(quote! {
354            utoipa::openapi::server::ServerBuilder::new()
355                .url(#url)
356                #description
357                #parameters
358                .build()
359        })
360    }
361}
362
363// ("username" = (default = "demo", description = "This is default username for the API")),
364// ("port" = (enum_values = (8080, 5000, 4545)))
365#[derive(Default)]
366#[cfg_attr(feature = "debug", derive(Debug))]
367struct ServerVariable {
368    name: String,
369    default: String,
370    description: Option<String>,
371    enum_values: Option<Punctuated<LitStr, Comma>>,
372}
373
374impl Parse for ServerVariable {
375    fn parse(input: ParseStream) -> syn::Result<Self> {
376        let variable_stream;
377        parenthesized!(variable_stream in input);
378        let mut server_variable = ServerVariable {
379            name: variable_stream.parse::<LitStr>()?.value(),
380            ..ServerVariable::default()
381        };
382
383        variable_stream.parse::<Token![=]>()?;
384        let content;
385        parenthesized!(content in variable_stream);
386
387        while !content.is_empty() {
388            let ident = content.parse::<Ident>()?;
389            let attribute_name = &*ident.to_string();
390
391            match attribute_name {
392                "default" => {
393                    server_variable.default =
394                        parse_utils::parse_next(&content, || content.parse::<LitStr>())?.value()
395                }
396                "description" => {
397                    server_variable.description =
398                        Some(parse_utils::parse_next(&content, || content.parse::<LitStr>())?.value())
399                }
400                "enum_values" => {
401                    server_variable.enum_values =
402                        Some(parse_utils::parse_comma_separated_within_parenthesis(&content)?)
403                }
404                _ => {
405                    return Err(Error::new(ident.span(), format!( "unexpected attribute: {attribute_name}, expected one of: default, description, enum_values")))
406                }
407            }
408
409            if !content.is_empty() {
410                content.parse::<Comma>()?;
411            }
412        }
413
414        Ok(server_variable)
415    }
416}
417
418pub(crate) struct OpenApi<'o>(pub Option<OpenApiAttr<'o>>, pub Ident);
419
420impl OpenApi<'_> {
421    fn nested_tokens(&self) -> Option<TokenStream> {
422        let nested = self.0.as_ref().map(|openapi| &openapi.nested)?;
423        let nest_tokens = nested.iter()
424                .map(|item| {
425                    let path = &item.path;
426                    let nest_api = &item
427                        .open_api
428                        .as_ref()
429                        .expect("type path of nested api is mandatory");
430                    let nest_api_ident = &nest_api
431                        .path
432                        .segments
433                        .last()
434                        .expect("nest api must have at least one segment")
435                        .ident;
436                    let nest_api_config = format_ident!("{}Config", nest_api_ident.to_string());
437
438                    let module_path = nest_api
439                        .path
440                        .segments
441                        .iter()
442                        .take(nest_api.path.segments.len() - 1)
443                        .map(|segment| segment.ident.to_string())
444                        .collect::<Vec<_>>()
445                        .join("::");
446                    let tags = &item.tags.iter().collect::<Array<_>>();
447
448                    let span = nest_api.span();
449                    quote_spanned! {span=>
450                        .nest(#path, {
451                            #[allow(non_camel_case_types)]
452                            struct #nest_api_config;
453                            impl utoipa::__dev::NestedApiConfig for #nest_api_config {
454                                fn config() -> (utoipa::openapi::OpenApi, Vec<&'static str>, &'static str) {
455                                    let api = <#nest_api as utoipa::OpenApi>::openapi();
456
457                                    (api, #tags.into(), #module_path)
458                                }
459                            }
460                            <#nest_api_config as utoipa::OpenApi>::openapi()
461                        })
462                    }
463                })
464                .collect::<TokenStream>();
465
466        if nest_tokens.is_empty() {
467            None
468        } else {
469            Some(nest_tokens)
470        }
471    }
472}
473
474impl ToTokensDiagnostics for OpenApi<'_> {
475    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) -> Result<(), Diagnostics> {
476        let OpenApi(attributes, ident) = self;
477
478        let info = Info::merge_with_env_args(
479            attributes
480                .as_ref()
481                .and_then(|attributes| attributes.info.clone()),
482        );
483
484        let components = attributes
485            .as_ref()
486            .map_try(|attributes| attributes.components.try_to_token_stream())?
487            .and_then(|tokens| {
488                if !tokens.is_empty() {
489                    Some(quote! { .components(Some(#tokens)) })
490                } else {
491                    None
492                }
493            });
494
495        let Paths(path_items, handlers) =
496            impl_paths(attributes.as_ref().map(|attributes| &attributes.paths));
497
498        let handler_schemas = handlers.iter().fold(
499            quote! {
500                    let components = openapi.components.get_or_insert(utoipa::openapi::Components::new());
501                    let mut schemas = Vec::<(String, utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>)>::new();
502            },
503            |mut handler_schemas, (usage, ..)| {
504                handler_schemas.extend(quote! {
505                    <#usage as utoipa::__dev::SchemaReferences>::schemas(&mut schemas);
506                });
507
508                handler_schemas
509            },
510        );
511
512        let securities = attributes
513            .as_ref()
514            .and_then(|openapi_attributes| openapi_attributes.security.as_ref())
515            .map(|securities| {
516                quote! {
517                    .security(Some(#securities))
518                }
519            });
520        let tags = attributes
521            .as_ref()
522            .and_then(|attributes| attributes.tags.as_ref())
523            .map(|tags| {
524                quote! {
525                    .tags(Some(#tags))
526                }
527            });
528        let external_docs = attributes
529            .as_ref()
530            .and_then(|attributes| attributes.external_docs.as_ref())
531            .map(|external_docs| {
532                quote! {
533                    .external_docs(Some(#external_docs))
534                }
535            });
536
537        let servers = match attributes.as_ref().map(|attributes| &attributes.servers) {
538            Some(servers) if !servers.is_empty() => {
539                let servers = servers.iter().collect::<Array<&Server>>();
540                Some(quote! { .servers(Some(#servers)) })
541            }
542            _ => None,
543        };
544
545        let modifiers_tokens = attributes
546            .as_ref()
547            .map(|attributes| &attributes.modifiers)
548            .map(|modifiers| {
549                let modifiers_len = modifiers.len();
550
551                quote! {
552                    let _mods: [&dyn utoipa::Modify; #modifiers_len] = [#modifiers];
553                    _mods.iter().for_each(|modifier| modifier.modify(&mut openapi));
554                }
555            });
556
557        let nested_tokens = self
558            .nested_tokens()
559            .map(|tokens| quote! {openapi = openapi #tokens;});
560        tokens.extend(quote! {
561            impl utoipa::OpenApi for #ident {
562                fn openapi() -> utoipa::openapi::OpenApi {
563                    use utoipa::{ToSchema, Path};
564                    let mut openapi = utoipa::openapi::OpenApiBuilder::new()
565                        .info(#info)
566                        .paths({
567                            #path_items
568                        })
569                        #components
570                        #securities
571                        #tags
572                        #servers
573                        #external_docs
574                        .build();
575                    #handler_schemas
576                    components.schemas.extend(schemas);
577                    #nested_tokens
578
579                    #modifiers_tokens
580
581                    openapi
582                }
583            }
584        });
585
586        Ok(())
587    }
588}
589
590#[derive(Default)]
591#[cfg_attr(feature = "debug", derive(Debug))]
592struct Components {
593    schemas: Vec<Schema>,
594    responses: Vec<Response>,
595}
596
597impl Parse for Components {
598    fn parse(input: ParseStream) -> syn::Result<Self> {
599        let content;
600        parenthesized!(content in input);
601        const EXPECTED_ATTRIBUTE: &str =
602            "unexpected attribute. expected one of: schemas, responses";
603
604        let mut schemas: Vec<Schema> = Vec::new();
605        let mut responses: Vec<Response> = Vec::new();
606
607        while !content.is_empty() {
608            let ident = content.parse::<Ident>().map_err(|error| {
609                Error::new(error.span(), format!("{EXPECTED_ATTRIBUTE}, {error}"))
610            })?;
611            let attribute = &*ident.to_string();
612
613            match attribute {
614                "schemas" => schemas.append(
615                    &mut parse_utils::parse_comma_separated_within_parenthesis(&content)?
616                        .into_iter()
617                        .collect(),
618                ),
619                "responses" => responses.append(
620                    &mut parse_utils::parse_comma_separated_within_parenthesis(&content)?
621                        .into_iter()
622                        .collect(),
623                ),
624                _ => return Err(syn::Error::new(ident.span(), EXPECTED_ATTRIBUTE)),
625            }
626
627            if !content.is_empty() {
628                content.parse::<Token![,]>()?;
629            }
630        }
631
632        Ok(Self { schemas, responses })
633    }
634}
635
636impl ToTokensDiagnostics for Components {
637    fn to_tokens(&self, tokens: &mut TokenStream) -> Result<(), Diagnostics> {
638        if self.schemas.is_empty() && self.responses.is_empty() {
639            return Ok(());
640        }
641
642        let builder_tokens = self
643            .schemas
644            .iter()
645            .map(|schema| match schema.get_component() {
646                Ok(component_schema) => Ok((component_schema, &schema.0)),
647                Err(diagnostics) => Err(diagnostics),
648            })
649            .collect::<Result<Vec<(ComponentSchema, &TypePath)>, Diagnostics>>()?
650            .into_iter()
651            .fold(
652                quote! { utoipa::openapi::ComponentsBuilder::new() },
653                |mut components, (component_schema, type_path)| {
654                    let schema = component_schema.to_token_stream();
655                    let name = &component_schema.name_tokens;
656
657                    components.extend(quote! { .schemas_from_iter( {
658                        let mut schemas = Vec::<(String, utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>)>::new();
659                        <#type_path as utoipa::ToSchema>::schemas(&mut schemas);
660                        schemas
661                    } )});
662                    components.extend(quote! { .schema(#name, {
663                        let mut generics = Vec::<utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>>::new();
664                        #schema
665                    }) });
666
667                    components
668                },
669            );
670
671        let builder_tokens =
672            self.responses
673                .iter()
674                .fold(builder_tokens, |mut builder_tokens, responses| {
675                    let Response(path) = responses;
676
677                    builder_tokens.extend(quote_spanned! {path.span() =>
678                        .response_from::<#path>()
679                    });
680                    builder_tokens
681                });
682
683        tokens.extend(quote! { #builder_tokens.build() });
684
685        Ok(())
686    }
687}
688
689struct Paths(TokenStream, Vec<(ExprPath, String, Ident)>);
690
691fn impl_paths(handler_paths: Option<&Punctuated<ExprPath, Comma>>) -> Paths {
692    let handlers = handler_paths
693        .into_iter()
694        .flatten()
695        .map(|handler| {
696            let segments = handler.path.segments.iter().collect::<Vec<_>>();
697            let handler_config_name = segments
698                .iter()
699                .map(|segment| segment.ident.to_string())
700                .collect::<Vec<_>>()
701                .join("_");
702            let handler_fn = &segments.last().unwrap().ident;
703            let handler_ident = path::format_path_ident(Cow::Borrowed(handler_fn));
704            let handler_ident_config = format_ident!("{}_config", handler_config_name);
705
706            let tag = segments
707                .iter()
708                .take(segments.len() - 1)
709                .map(|part| part.ident.to_string())
710                .collect::<Vec<_>>()
711                .join("::");
712
713            let usage = syn::parse_str::<ExprPath>(
714                &vec![
715                    if tag.is_empty() { None } else { Some(&*tag) },
716                    Some(&*handler_ident.as_ref().to_string()),
717                ]
718                .into_iter()
719                .flatten()
720                .collect::<Vec<_>>()
721                .join("::"),
722            )
723            .unwrap();
724            (usage, tag, handler_ident_config)
725        })
726        .collect::<Vec<_>>();
727
728    let handlers_impls = handlers
729        .iter()
730        .map(|(usage, tag, handler_ident_nested)| {
731            quote! {
732                #[allow(non_camel_case_types)]
733                struct #handler_ident_nested;
734                #[allow(non_camel_case_types)]
735                impl utoipa::__dev::PathConfig for #handler_ident_nested {
736                    fn path() -> String {
737                        #usage::path()
738                    }
739                    fn methods() -> Vec<utoipa::openapi::path::HttpMethod> {
740                        #usage::methods()
741                    }
742                    fn tags_and_operation() -> (Vec<&'static str>, utoipa::openapi::path::Operation) {
743                        let item = #usage::operation();
744                        let mut tags = <#usage as utoipa::__dev::Tags>::tags();
745                        if !#tag.is_empty() && tags.is_empty() {
746                            tags.push(#tag);
747                        }
748
749                        (tags, item)
750                    }
751                }
752            }
753        })
754        .collect::<TokenStream>();
755
756    let tokens = handler_paths.into_iter().flatten().fold(
757        quote! { #handlers_impls utoipa::openapi::path::PathsBuilder::new() },
758        |mut paths, handler| {
759            let segments = handler.path.segments.iter().collect::<Vec<_>>();
760            let handler_config_name = segments
761                .iter()
762                .map(|segment| segment.ident.to_string())
763                .collect::<Vec<_>>()
764                .join("_");
765            let handler_ident_config = format_ident!("{}_config", handler_config_name);
766
767            paths.extend(quote! {
768                .path_from::<#handler_ident_config>()
769            });
770
771            paths
772        },
773    );
774
775    Paths(tokens, handlers)
776}
777
778/// (path = "/nest/path", api = NestApi, tags = ["tag1", "tag2"])
779#[cfg_attr(feature = "debug", derive(Debug))]
780#[derive(Default)]
781struct NestOpenApi {
782    path: parse_utils::LitStrOrExpr,
783    open_api: Option<TypePath>,
784    tags: Punctuated<parse_utils::LitStrOrExpr, Comma>,
785}
786
787impl Parse for NestOpenApi {
788    fn parse(input: ParseStream) -> syn::Result<Self> {
789        const ERROR_MESSAGE: &str = "unexpected identifier, expected any of: path, api, tags";
790        let mut nest = NestOpenApi::default();
791
792        while !input.is_empty() {
793            let ident = input.parse::<Ident>().map_err(|error| {
794                syn::Error::new(error.span(), format!("{ERROR_MESSAGE}: {error}"))
795            })?;
796
797            match &*ident.to_string() {
798                "path" => nest.path = parse_utils::parse_next_literal_str_or_expr(input)?,
799                "api" => nest.open_api = Some(parse_utils::parse_next(input, || input.parse())?),
800                "tags" => {
801                    nest.tags = parse_utils::parse_next(input, || {
802                        let tags;
803                        bracketed!(tags in input);
804                        Punctuated::parse_terminated(&tags)
805                    })?;
806                }
807                _ => return Err(syn::Error::new(ident.span(), ERROR_MESSAGE)),
808            }
809
810            if !input.is_empty() {
811                input.parse::<Token![,]>()?;
812            }
813        }
814        if nest.path.is_empty_litstr() {
815            return Err(syn::Error::new(
816                input.span(),
817                "`path = ...` argument is mandatory for nest(...) statement",
818            ));
819        }
820        if nest.open_api.is_none() {
821            return Err(syn::Error::new(
822                input.span(),
823                "`api = ...` argument is mandatory for nest(...) statement",
824            ));
825        }
826
827        Ok(nest)
828    }
829}