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