1use std::borrow::Cow;
2use std::ops::Deref;
3use std::{io::Error, str::FromStr};
4
5use proc_macro2::{Ident, Span, TokenStream as TokenStream2};
6use proc_macro_error::abort;
7use quote::{format_ident, quote, quote_spanned, ToTokens};
8use syn::punctuated::Punctuated;
9use syn::spanned::Spanned;
10use syn::token::Paren;
11use syn::{parenthesized, parse::Parse, Token};
12use syn::{Expr, ExprLit, Lit, LitStr, Type};
13
14use crate::component::{GenericType, TypeTree};
15use crate::path::request_body::RequestBody;
16use crate::{parse_utils, Deprecated};
17use crate::{schema_type::SchemaType, security_requirement::SecurityRequirementAttr, Array};
18
19use self::response::Response;
20use self::{parameter::Parameter, request_body::RequestBodyAttr, response::Responses};
21
22pub mod example;
23pub mod parameter;
24mod request_body;
25pub mod response;
26mod status;
27
28pub(crate) const PATH_STRUCT_PREFIX: &str = "__path_";
29
30#[derive(Default)]
31#[cfg_attr(feature = "debug", derive(Debug))]
32pub struct PathAttr<'p> {
33 path_operation: Option<PathOperation>,
34 request_body: Option<RequestBody<'p>>,
35 responses: Vec<Response<'p>>,
36 pub(super) path: Option<String>,
37 operation_id: Option<Expr>,
38 tag: Option<String>,
39 params: Vec<Parameter<'p>>,
40 security: Option<Array<'p, SecurityRequirementAttr>>,
41 context_path: Option<String>,
42}
43
44impl<'p> PathAttr<'p> {
45 #[cfg(feature = "auto_into_responses")]
46 pub fn responses_from_into_responses(&mut self, ty: &'p syn::TypePath) {
47 self.responses
48 .push(Response::IntoResponses(Cow::Borrowed(ty)))
49 }
50
51 #[cfg(any(
52 feature = "actix_extras",
53 feature = "rocket_extras",
54 feature = "axum_extras"
55 ))]
56 pub fn update_request_body(&mut self, request_body: Option<crate::ext::RequestBody<'p>>) {
57 use std::mem;
58
59 if self.request_body.is_none() {
60 self.request_body = request_body
61 .map(RequestBody::Ext)
62 .or(mem::take(&mut self.request_body));
63 }
64 }
65
66 #[cfg(any(
68 feature = "actix_extras",
69 feature = "rocket_extras",
70 feature = "axum_extras"
71 ))]
72 pub fn update_parameters_ext<I: IntoIterator<Item = Parameter<'p>>>(
73 &mut self,
74 ext_parameters: I,
75 ) {
76 let ext_params = ext_parameters.into_iter();
77
78 let (existing_params, new_params): (Vec<Parameter>, Vec<Parameter>) =
79 ext_params.partition(|param| self.params.iter().any(|p| p == param));
80
81 for existing in existing_params {
82 if let Some(param) = self.params.iter_mut().find(|p| **p == existing) {
83 param.merge(existing);
84 }
85 }
86
87 self.params.extend(
88 new_params
89 .into_iter()
90 .filter(|param| !matches!(param, Parameter::IntoParamsIdent(_))),
91 );
92 }
93}
94
95impl Parse for PathAttr<'_> {
96 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
97 const EXPECTED_ATTRIBUTE_MESSAGE: &str = "unexpected identifier, expected any of: operation_id, path, get, post, put, delete, options, head, patch, trace, connect, request_body, responses, params, tag, security, context_path";
98 let mut path_attr = PathAttr::default();
99
100 while !input.is_empty() {
101 let ident = input.parse::<Ident>().map_err(|error| {
102 syn::Error::new(
103 error.span(),
104 format!("{EXPECTED_ATTRIBUTE_MESSAGE}, {error}"),
105 )
106 })?;
107 let attribute_name = &*ident.to_string();
108
109 match attribute_name {
110 "operation_id" => {
111 path_attr.operation_id =
112 Some(parse_utils::parse_next(input, || Expr::parse(input))?);
113 }
114 "path" => {
115 path_attr.path = Some(parse_utils::parse_next_literal_str(input)?);
116 }
117 "request_body" => {
118 path_attr.request_body =
119 Some(RequestBody::Parsed(input.parse::<RequestBodyAttr>()?));
120 }
121 "responses" => {
122 let responses;
123 parenthesized!(responses in input);
124 path_attr.responses =
125 Punctuated::<Response, Token![,]>::parse_terminated(&responses)
126 .map(|punctuated| punctuated.into_iter().collect::<Vec<Response>>())?;
127 }
128 "params" => {
129 let params;
130 parenthesized!(params in input);
131 path_attr.params =
132 Punctuated::<Parameter, Token![,]>::parse_terminated(¶ms)
133 .map(|punctuated| punctuated.into_iter().collect::<Vec<Parameter>>())?;
134 }
135 "tag" => {
136 path_attr.tag = Some(parse_utils::parse_next_literal_str(input)?);
137 }
138 "security" => {
139 let security;
140 parenthesized!(security in input);
141 path_attr.security = Some(parse_utils::parse_groups(&security)?)
142 }
143 "context_path" => {
144 path_attr.context_path = Some(parse_utils::parse_next_literal_str(input)?)
145 }
146 _ => {
147 if let Some(path_operation) =
149 attribute_name.parse::<PathOperation>().into_iter().next()
150 {
151 path_attr.path_operation = Some(path_operation)
152 } else {
153 return Err(syn::Error::new(ident.span(), EXPECTED_ATTRIBUTE_MESSAGE));
154 }
155 }
156 }
157
158 if !input.is_empty() {
159 input.parse::<Token![,]>()?;
160 }
161 }
162
163 Ok(path_attr)
164 }
165}
166
167#[cfg_attr(feature = "debug", derive(Debug))]
179pub enum PathOperation {
180 Get,
181 Post,
182 Put,
183 Delete,
184 Options,
185 Head,
186 Patch,
187 Trace,
188 Connect,
189}
190
191impl PathOperation {
192 #[cfg(any(feature = "actix_extras", feature = "rocket_extras"))]
196 pub fn from_ident(ident: &Ident) -> Self {
197 match ident.to_string().as_str().parse::<PathOperation>() {
198 Ok(operation) => operation,
199 Err(error) => abort!(ident.span(), format!("{error}")),
200 }
201 }
202}
203
204impl FromStr for PathOperation {
205 type Err = Error;
206
207 fn from_str(s: &str) -> Result<Self, Self::Err> {
208 match s {
209 "get" => Ok(Self::Get),
210 "post" => Ok(Self::Post),
211 "put" => Ok(Self::Put),
212 "delete" => Ok(Self::Delete),
213 "options" => Ok(Self::Options),
214 "head" => Ok(Self::Head),
215 "patch" => Ok(Self::Patch),
216 "trace" => Ok(Self::Trace),
217 "connect" => Ok(Self::Connect),
218 _ => Err(Error::new(
219 std::io::ErrorKind::Other,
220 "invalid PathOperation expected one of: get, post, put, delete, options, head, patch, trace, connect",
221 )),
222 }
223 }
224}
225
226impl ToTokens for PathOperation {
227 fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
228 let path_item_type = match self {
229 Self::Get => quote! { utoipa::openapi::PathItemType::Get },
230 Self::Post => quote! { utoipa::openapi::PathItemType::Post },
231 Self::Put => quote! { utoipa::openapi::PathItemType::Put },
232 Self::Delete => quote! { utoipa::openapi::PathItemType::Delete },
233 Self::Options => quote! { utoipa::openapi::PathItemType::Options },
234 Self::Head => quote! { utoipa::openapi::PathItemType::Head },
235 Self::Patch => quote! { utoipa::openapi::PathItemType::Patch },
236 Self::Trace => quote! { utoipa::openapi::PathItemType::Trace },
237 Self::Connect => quote! { utoipa::openapi::PathItemType::Connect },
238 };
239
240 tokens.extend(path_item_type);
241 }
242}
243pub struct Path<'p> {
244 path_attr: PathAttr<'p>,
245 fn_name: String,
246 path_operation: Option<PathOperation>,
247 path: Option<String>,
248 doc_comments: Option<Vec<String>>,
249 deprecated: Option<bool>,
250}
251
252impl<'p> Path<'p> {
253 pub fn new(path_attr: PathAttr<'p>, fn_name: &str) -> Self {
254 Self {
255 path_attr,
256 fn_name: fn_name.to_string(),
257 path_operation: None,
258 path: None,
259 doc_comments: None,
260 deprecated: None,
261 }
262 }
263
264 pub fn path_operation(mut self, path_operation: Option<PathOperation>) -> Self {
265 self.path_operation = path_operation;
266
267 self
268 }
269
270 pub fn path(mut self, path_provider: impl FnOnce() -> Option<String>) -> Self {
271 self.path = path_provider();
272
273 self
274 }
275
276 pub fn doc_comments(mut self, doc_comments: Vec<String>) -> Self {
277 self.doc_comments = Some(doc_comments);
278
279 self
280 }
281
282 pub fn deprecated(mut self, deprecated: Option<bool>) -> Self {
283 self.deprecated = deprecated;
284
285 self
286 }
287}
288
289impl<'p> ToTokens for Path<'p> {
290 fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
291 let path_struct = format_ident!("{}{}", PATH_STRUCT_PREFIX, self.fn_name);
292 let operation_id = self
293 .path_attr
294 .operation_id
295 .clone()
296 .or(Some(ExprLit {
297 attrs: vec![],
298 lit: Lit::Str(LitStr::new(&self.fn_name, Span::call_site()))
299 }.into()))
300 .unwrap_or_else(|| {
301 abort! {
302 Span::call_site(), "operation id is not defined for path";
303 help = r###"Try to define it in #[utoipa::path(operation_id = {})]"###, &self.fn_name;
304 help = "Did you define the #[utoipa::path(...)] over function?"
305 }
306 });
307 let tag = &*self
308 .path_attr
309 .tag
310 .as_ref()
311 .map(ToOwned::to_owned)
312 .unwrap_or_default();
313 let path_operation = self
314 .path_attr
315 .path_operation
316 .as_ref()
317 .or(self.path_operation.as_ref())
318 .unwrap_or_else(|| {
319 #[cfg(any(feature = "actix_extras", feature = "rocket_extras"))]
320 let help =
321 Some("Did you forget to define operation path attribute macro e.g #[get(...)]");
322
323 #[cfg(not(any(feature = "actix_extras", feature = "rocket_extras")))]
324 let help = None::<&str>;
325
326 abort! {
327 Span::call_site(), "path operation is not defined for path";
328 help = "Did you forget to define it in #[utoipa::path(get,...)]";
329 help =? help
330 }
331 });
332
333 let path = self
334 .path_attr
335 .path
336 .as_ref()
337 .or(self.path.as_ref())
338 .unwrap_or_else(|| {
339 #[cfg(any(feature = "actix_extras", feature = "rocket_extras"))]
340 let help =
341 Some("Did you forget to define operation path attribute macro e.g #[get(...)]");
342
343 #[cfg(not(any(feature = "actix_extras", feature = "rocket_extras")))]
344 let help = None::<&str>;
345
346 abort! {
347 Span::call_site(), "path is not defined for path";
348 help = r###"Did you forget to define it in #[utoipa::path(path = "...")]"###;
349 help =? help
350 }
351 });
352
353 let path_with_context_path = self
354 .path_attr
355 .context_path
356 .as_ref()
357 .map(|context_path| format!("{context_path}{path}"))
358 .unwrap_or_else(|| path.to_string());
359
360 let operation: Operation = Operation {
361 deprecated: &self.deprecated,
362 operation_id,
363 summary: self
364 .doc_comments
365 .as_ref()
366 .and_then(|comments| comments.iter().next()),
367 description: self.doc_comments.as_ref(),
368 parameters: self.path_attr.params.as_ref(),
369 request_body: self.path_attr.request_body.as_ref(),
370 responses: self.path_attr.responses.as_ref(),
371 security: self.path_attr.security.as_ref(),
372 };
373
374 tokens.extend(quote! {
375 #[allow(non_camel_case_types)]
376 #[doc(hidden)]
377 pub struct #path_struct;
378
379 impl utoipa::Path for #path_struct {
380 fn path() -> &'static str {
381 #path_with_context_path
382 }
383
384 fn path_item(default_tag: Option<&str>) -> utoipa::openapi::path::PathItem {
385 use utoipa::openapi::ToArray;
386 use std::iter::FromIterator;
387 utoipa::openapi::PathItem::new(
388 #path_operation,
389 #operation.tag(*[Some(#tag), default_tag, Some("crate")].iter()
390 .flatten()
391 .find(|t| !t.is_empty()).unwrap()
392 )
393 )
394 }
395 }
396 });
397 }
398}
399
400#[cfg_attr(feature = "debug", derive(Debug))]
401struct Operation<'a> {
402 operation_id: Expr,
403 summary: Option<&'a String>,
404 description: Option<&'a Vec<String>>,
405 deprecated: &'a Option<bool>,
406 parameters: &'a Vec<Parameter<'a>>,
407 request_body: Option<&'a RequestBody<'a>>,
408 responses: &'a Vec<Response<'a>>,
409 security: Option<&'a Array<'a, SecurityRequirementAttr>>,
410}
411
412impl ToTokens for Operation<'_> {
413 fn to_tokens(&self, tokens: &mut TokenStream2) {
414 tokens.extend(quote! { utoipa::openapi::path::OperationBuilder::new() });
415
416 if let Some(request_body) = self.request_body {
417 tokens.extend(quote! {
418 .request_body(Some(#request_body))
419 })
420 }
421
422 let responses = Responses(self.responses);
423 tokens.extend(quote! {
424 .responses(#responses)
425 });
426 if let Some(security_requirements) = self.security {
427 tokens.extend(quote! {
428 .securities(Some(#security_requirements))
429 })
430 }
431 let operation_id = &self.operation_id;
432 tokens.extend(quote_spanned! { operation_id.span() =>
433 .operation_id(Some(#operation_id))
434 });
435
436 if let Some(deprecated) = self.deprecated.map(Into::<Deprecated>::into) {
437 tokens.extend(quote!( .deprecated(Some(#deprecated))))
438 }
439
440 if let Some(summary) = self.summary {
441 tokens.extend(quote! {
442 .summary(Some(#summary))
443 })
444 }
445
446 if let Some(description) = self.description {
447 let description = description.join("\n");
448
449 if !description.is_empty() {
450 tokens.extend(quote! {
451 .description(Some(#description))
452 })
453 }
454 }
455
456 self.parameters
457 .iter()
458 .for_each(|parameter| parameter.to_tokens(tokens));
459 }
460}
461
462#[cfg_attr(feature = "debug", derive(Debug))]
464enum PathType<'p> {
465 Ref(String),
466 MediaType(InlineType<'p>),
467 InlineSchema(TokenStream2, Type),
468}
469
470impl Parse for PathType<'_> {
471 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
472 let fork = input.fork();
473 let is_ref = if (fork.parse::<Option<Token![ref]>>()?).is_some() {
474 fork.peek(Paren)
475 } else {
476 false
477 };
478
479 if is_ref {
480 input.parse::<Token![ref]>()?;
481 let ref_stream;
482 parenthesized!(ref_stream in input);
483 Ok(Self::Ref(ref_stream.parse::<LitStr>()?.value()))
484 } else {
485 Ok(Self::MediaType(input.parse()?))
486 }
487 }
488}
489
490#[cfg_attr(feature = "debug", derive(Debug))]
492struct InlineType<'i> {
493 ty: Cow<'i, Type>,
494 is_inline: bool,
495}
496
497impl InlineType<'_> {
498 fn as_type_tree(&self) -> TypeTree {
500 TypeTree::from_type(&self.ty)
501 }
502}
503
504impl Parse for InlineType<'_> {
505 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
506 let fork = input.fork();
507 let is_inline = if let Some(ident) = fork.parse::<Option<Ident>>()? {
508 ident == "inline" && fork.peek(Paren)
509 } else {
510 false
511 };
512
513 let ty = if is_inline {
514 input.parse::<Ident>()?;
515 let inlined;
516 parenthesized!(inlined in input);
517
518 inlined.parse::<Type>()?
519 } else {
520 input.parse::<Type>()?
521 };
522
523 Ok(InlineType {
524 ty: Cow::Owned(ty),
525 is_inline,
526 })
527 }
528}
529
530pub trait PathTypeTree {
531 fn get_default_content_type(&self) -> &str;
533
534 fn is_option(&self) -> bool;
536
537 fn is_array(&self) -> bool;
539}
540
541impl PathTypeTree for TypeTree<'_> {
542 fn get_default_content_type(&self) -> &'static str {
544 if self.is_array()
545 && self
546 .children
547 .as_ref()
548 .map(|children| {
549 children
550 .iter()
551 .flat_map(|child| &child.path)
552 .any(|path| SchemaType(path).is_byte())
553 })
554 .unwrap_or(false)
555 {
556 "application/octet-stream"
557 } else if self
558 .path
559 .as_ref()
560 .map(|path| SchemaType(path.deref()))
561 .map(|schema_type| schema_type.is_primitive())
562 .unwrap_or(false)
563 {
564 "text/plain"
565 } else {
566 "application/json"
567 }
568 }
569
570 fn is_option(&self) -> bool {
572 matches!(self.generic_type, Some(GenericType::Option))
573 }
574
575 fn is_array(&self) -> bool {
577 match self.generic_type {
578 Some(GenericType::Vec) => true,
579 Some(_) => self
580 .children
581 .as_ref()
582 .unwrap()
583 .iter()
584 .any(|child| child.is_array()),
585 None => false,
586 }
587 }
588}