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#[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#[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#[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}