1use heck::*;
5use http::StatusCode;
6use lazy_static::lazy_static;
7use proc_macro::TokenStream;
8use quote::{quote, ToTokens};
9use strum_macros::EnumString;
10use syn::{
11 parse_macro_input,
12 punctuated::{Pair, Punctuated},
13 spanned::Spanned,
14 Attribute, Data, DataEnum, DeriveInput, Field, Fields, FieldsNamed, FieldsUnnamed, FnArg,
15 Generics, Ident, ItemFn, Lit, Meta, MetaList, MetaNameValue, NestedMeta, Path, PathArguments,
16 ReturnType, Token, TraitBound, Type, TypeTraitObject,
17};
18
19use proc_macro2::TokenStream as TokenStream2;
20use std::collections::HashMap;
21
22const SCHEMA_MACRO_ATTR: &str = "openapi";
23
24lazy_static! {
25 static ref EMPTY_SCHEMA_HELP: String = format!(
26 "you can mark the struct with #[{}(empty)] to ignore this warning.",
27 SCHEMA_MACRO_ATTR
28 );
29}
30
31pub fn emit_v2_operation(attrs: TokenStream, input: TokenStream) -> TokenStream {
33 let default_span = proc_macro2::Span::call_site();
34 let mut item_ast: ItemFn = match syn::parse(input) {
35 Ok(s) => s,
36 Err(e) => {
37 emit_error!(e.span().unwrap(), "operation must be a function.");
38 return quote!().into();
39 }
40 };
41
42 let s_name = format!("paperclip_{}", item_ast.sig.ident);
44 let unit_struct = Ident::new(&s_name, default_span);
45 let generics = &item_ast.sig.generics;
46 let mut generics_call = quote!();
47 let mut struct_definition = quote!(
48 #[allow(non_camel_case_types, missing_docs)]
49 struct #unit_struct;
50 );
51 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
52 if !generics.params.is_empty() {
53 let turbofish = ty_generics.as_turbofish();
54 let generics_params = extract_generics_params(&item_ast);
55 generics_call = quote!(#turbofish { p: std::marker::PhantomData });
56 struct_definition = quote!(struct #unit_struct #ty_generics { p: std::marker::PhantomData<(#generics_params)> } )
57 }
58
59 if item_ast.sig.asyncness.is_some() {
61 item_ast.sig.asyncness = None;
62 }
63
64 let mut wrapper = quote!(paperclip::actix::ResponseWrapper<actix_web::HttpResponse, #unit_struct #ty_generics>);
65 let mut is_impl_trait = false;
66 let mut is_responder = false;
67 match &mut item_ast.sig.output {
68 rt @ ReturnType::Default => {
69 *rt = ReturnType::Type(
71 Token,
72 Box::new(syn::parse2(wrapper.clone()).expect("parsing empty type")),
73 );
74 }
75 ReturnType::Type(_, ty) => {
76 let t = quote!(#ty).to_string();
77 if let Type::ImplTrait(_) = &**ty {
78 is_impl_trait = true;
79 }
80
81 if t == "impl Responder" {
82 is_responder = true;
85 *ty = syn::parse2(quote!(
86 impl std::future::Future<Output=paperclip::actix::ResponderWrapper<#ty>>
87 ))
88 .expect("parsing impl trait");
89 } else if !is_impl_trait {
90 *ty = syn::parse2(quote!(impl std::future::Future<Output=#ty>))
92 .expect("parsing impl trait");
93 }
94
95 if let Type::ImplTrait(imp) = &**ty {
96 let obj = TypeTraitObject {
97 dyn_token: Some(Token),
98 bounds: imp.bounds.clone(),
99 };
100 *ty = syn::parse2(quote!(#ty + paperclip::v2::schema::Apiv2Operation))
101 .expect("parsing impl trait");
102
103 if !is_responder {
104 wrapper = quote!(paperclip::actix::ResponseWrapper<Box<#obj + std::marker::Unpin>, #unit_struct #ty_generics>);
107 }
108 }
109 }
110 }
111
112 let block = item_ast.block;
113 let wrapped_fn_call = if is_responder {
115 quote!(paperclip::util::ready(paperclip::actix::ResponderWrapper((move || #block)())))
116 } else if is_impl_trait {
117 quote!((move || #block)())
118 } else {
119 quote!((move || async move #block)())
120 };
121
122 item_ast.block = Box::new(
123 syn::parse2(quote!(
124 {
125 let f = #wrapped_fn_call;
126 paperclip::actix::ResponseWrapper {
127 responder: f,
128 operations: #unit_struct #generics_call,
129 }
130 }
131 ))
132 .expect("parsing wrapped block"),
133 );
134
135 let (mut op_params, mut op_values) = parse_operation_attrs(attrs);
137
138 if op_params.iter().any(|i| *i == "skip") {
139 return quote!(
140 #[allow(non_camel_case_types, missing_docs)]
141 #struct_definition
142
143 #item_ast
144
145 impl #impl_generics paperclip::v2::schema::Apiv2Operation for #unit_struct #ty_generics #where_clause {
146 fn operation() -> paperclip::v2::models::DefaultOperationRaw {
147 Default::default()
148 }
149
150 #[allow(unused_mut)]
151 fn security_definitions() -> std::collections::BTreeMap<String, paperclip::v2::models::SecurityScheme> {
152 Default::default()
153 }
154
155 fn definitions() -> std::collections::BTreeMap<String, paperclip::v2::models::DefaultSchemaRaw> {
156 Default::default()
157 }
158
159 fn is_visible() -> bool {
160 false
161 }
162 }
163 ).into();
164 }
165
166 if !op_params.iter().any(|i| *i == "summary") {
168 let (summary, description) = extract_fn_documentation(&item_ast);
169 if let Some(summary) = summary {
170 op_params.push(Ident::new("summary", item_ast.span()));
171 op_values.push(summary)
172 }
173 if let Some(description) = description {
174 op_params.push(Ident::new("description", item_ast.span()));
175 op_values.push(description)
176 }
177 }
178
179 if op_params.iter().any(|i| *i == "deprecated") || extract_deprecated(&item_ast.attrs) {
180 op_params.push(Ident::new("deprecated", item_ast.span()));
181 op_values.push(quote!(true))
182 }
183
184 let modifiers = extract_fn_arguments_types(&item_ast);
185
186 let operation_modifier = if is_responder {
187 quote! { paperclip::actix::ResponderWrapper::<actix_web::HttpResponse> }
188 } else {
189 quote! { <<#wrapper as std::future::Future>::Output> }
190 };
191
192 quote!(
193 #struct_definition
194
195 #item_ast
196
197 impl #impl_generics paperclip::v2::schema::Apiv2Operation for #unit_struct #ty_generics #where_clause {
198 fn operation() -> paperclip::v2::models::DefaultOperationRaw {
199 use paperclip::actix::OperationModifier;
200 let mut op = paperclip::v2::models::DefaultOperationRaw {
201 #(
202 #op_params: #op_values,
203 )*
204 .. Default::default()
205 };
206 #(
207 <#modifiers>::update_parameter(&mut op);
208 <#modifiers>::update_security(&mut op);
209 )*
210 #operation_modifier::update_response(&mut op);
211 op
212 }
213
214 #[allow(unused_mut)]
215 fn security_definitions() -> std::collections::BTreeMap<String, paperclip::v2::models::SecurityScheme> {
216 use paperclip::actix::OperationModifier;
217 let mut map = Default::default();
218 #(
219 <#modifiers>::update_security_definitions(&mut map);
220 )*
221 map
222 }
223
224 fn definitions() -> std::collections::BTreeMap<String, paperclip::v2::models::DefaultSchemaRaw> {
225 use paperclip::actix::OperationModifier;
226 let mut map = std::collections::BTreeMap::new();
227 #(
228 <#modifiers>::update_definitions(&mut map);
229 )*
230 #operation_modifier::update_definitions(&mut map);
231 map
232 }
233 }
234 )
235 .into()
236}
237
238fn extract_generics_params(item_ast: &ItemFn) -> Punctuated<Ident, syn::token::Comma> {
240 item_ast
241 .sig
242 .generics
243 .params
244 .pairs()
245 .filter_map(|pair| match pair {
246 Pair::Punctuated(syn::GenericParam::Type(gen), punct) => {
247 Some(Pair::new(gen.ident.clone(), Some(*punct)))
248 }
249 Pair::End(syn::GenericParam::Type(gen)) => Some(Pair::new(gen.ident.clone(), None)),
250 _ => None,
251 })
252 .collect()
253}
254
255fn extract_fn_arguments_types(item_ast: &ItemFn) -> Vec<Type> {
257 item_ast
258 .sig
259 .inputs
260 .iter()
261 .filter_map(|inp| match inp {
262 FnArg::Receiver(_) => None,
263 FnArg::Typed(ref t) => Some(*t.ty.clone()),
264 })
265 .collect()
266}
267
268fn parse_operation_attrs(attrs: TokenStream) -> (Vec<Ident>, Vec<proc_macro2::TokenStream>) {
273 let attrs = crate::parse_input_attrs(attrs);
274 let mut params = Vec::new();
275 let mut values = Vec::new();
276 for attr in attrs.0 {
277 match &attr {
278 NestedMeta::Meta(Meta::Path(attr_path)) => {
279 if let Some(attr_) = attr_path.get_ident() {
280 if *attr_ == "skip" || *attr_ == "deprecated" {
281 params.push(attr_.clone());
282 } else {
283 emit_error!(attr_.span(), "Not supported bare attribute {:?}", attr_)
284 }
285 }
286 }
287 NestedMeta::Meta(Meta::NameValue(MetaNameValue { path, lit, .. })) => {
288 if let Some(ident) = path.get_ident() {
289 match ident.to_string().as_str() {
290 "summary" | "description" | "operation_id" => {
291 if let Lit::Str(val) = lit {
292 params.push(ident.clone());
293 values.push(quote!(Some(# val.to_string())));
294 } else {
295 emit_error!(lit.span(), "Expected string literal: {:?}", lit)
296 }
297 }
298 "consumes" | "produces" => {
299 if let Lit::Str(mimes) = lit {
300 let mut mime_types = Vec::new();
301 for val in mimes.value().split(',') {
302 let val = val.trim();
303 if let Err(err) = val.parse::<mime::Mime>() {
304 emit_error!(
305 lit.span(),
306 "Value {} does not parse as mime type: {}",
307 val,
308 err
309 );
310 } else {
311 mime_types.push(quote!(paperclip::v2::models::MediaRange( # val.parse().unwrap())));
312 }
313 }
314 if !mime_types.is_empty() {
315 params.push(ident.clone());
316 values.push(quote!({
317 let mut tmp = std::collections::BTreeSet::new();
318 # (
319 tmp.insert(# mime_types);
320 ) *
321 Some(tmp)
322 }));
323 }
324 } else {
325 emit_error!(
326 lit.span(),
327 "Expected comma separated values in string literal: {:?}",
328 lit
329 )
330 }
331 }
332 x => emit_error!(ident.span(), "Unknown attribute {}", x),
333 }
334 } else {
335 emit_error!(
336 path.span(),
337 "Expected single identifier, got path {:?}",
338 path
339 )
340 }
341 }
342 NestedMeta::Meta(Meta::List(MetaList { path, nested, .. })) => {
343 if let Some(ident) = path.get_ident() {
344 match ident.to_string().as_str() {
345 "tags" => {
346 let mut tags = Vec::new();
347 for meta in nested.pairs().map(|pair| pair.into_value()) {
348 if let NestedMeta::Meta(Meta::Path(Path { segments, .. })) = meta {
349 tags.push(segments[0].ident.to_string());
350 } else if let NestedMeta::Lit(Lit::Str(lit)) = meta {
351 tags.push(lit.value());
352 } else {
353 emit_error!(
354 meta.span(),
355 "Expected comma separated list of tags idents: {:?}",
356 meta
357 )
358 }
359 }
360 if !tags.is_empty() {
361 params.push(ident.clone());
362 values.push(quote!(vec![ #( #tags.to_string() ),* ]));
363 }
364 }
365 x => emit_error!(ident.span(), "Unknown list ident {}", x),
366 }
367 }
368 }
369 _ => {
370 emit_error!(attr.span(), "Not supported attribute type {:?}", attr)
371 }
372 }
373 }
374 (params, values)
375}
376
377fn extract_fn_documentation(
379 item_ast: &ItemFn,
380) -> (
381 Option<proc_macro2::TokenStream>,
382 Option<proc_macro2::TokenStream>,
383) {
384 let docs = extract_documentation(&item_ast.attrs);
385 let lines = docs.lines();
386 let mut before_empty = true;
387 let (summary, description): (Vec<_>, Vec<_>) = lines.partition(|line| {
388 if line.trim().is_empty() {
389 before_empty = false
390 };
391 before_empty
392 });
393 let none_if_empty = |text: &str| {
394 if text.is_empty() {
395 None
396 } else {
397 Some(quote!(Some(#text.to_string())))
398 }
399 };
400 let summary = none_if_empty(summary.into_iter().collect::<String>().trim());
401 let description = none_if_empty(description.join("\n").trim());
402 (summary, description)
403}
404
405pub fn emit_v2_errors(attrs: TokenStream, input: TokenStream) -> TokenStream {
407 let item_ast = match crate::expect_struct_or_enum(input) {
408 Ok(i) => i,
409 Err(ts) => return ts,
410 };
411
412 let name = &item_ast.ident;
413 let attrs = crate::parse_input_attrs(attrs);
414 let generics = item_ast.generics.clone();
415 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
416
417 let mut default_schema: Option<syn::Ident> = None;
418 let error_codes = attrs
420 .0
421 .iter()
422 .fold(Vec::new(), |mut list: Vec<(Option<u16>, Option<String>, Option<syn::Ident>, _)>, attr| {
424 let span = attr.span().unwrap();
425 match attr {
426 NestedMeta::Meta(Meta::NameValue(name_value)) => {
428 let attr_name = name_value.path.get_ident().map(|ident| ident.to_string());
429 let attr_value = &name_value.lit;
430 match (attr_name.as_deref(), attr_value) {
431 (Some("code"), Lit::Int(attr_value)) => {
433 let status_code = attr_value.base10_parse::<u16>()
434 .map_err(|_| emit_error!(span, "Invalid u16 in code argument")).ok();
435 list.push((status_code, None, None, attr));
436 }
437 (Some("description"), Lit::Str(attr_value)) =>
439 if let Some(last_value) = list.last_mut() {
440 if last_value.1.is_some() {
441 emit_warning!(span, "This attribute overwrites previous description");
442 }
443 last_value.1 = Some(attr_value.value());
444 } else {
445 emit_error!(span, "Attribute 'description' can be only placed after prior 'code' argument");
446 },
447 (Some("schema"), Lit::Str(attr_value)) =>
449 if let Some(last_value) = list.last_mut() {
450 if last_value.2.is_some() {
451 emit_warning!(span, "This attribute overwrites previous schema");
452 }
453 match attr_value.parse() {
454 Ok(value) => last_value.2 = Some(value),
455 Err(error) => emit_error!(span, "Error parsing schema: {}", error),
456 }
457 } else {
458 emit_error!(span, "Attribute 'schema' can be only placed after prior 'code' argument");
459 },
460 (Some("default_schema"), Lit::Str(attr_value)) =>
461 match attr_value.parse() {
462 Ok(value) => default_schema = Some(value),
463 Err(error) => emit_error!(span, "Error parsing default_schema: {}", error),
464 },
465 _ => emit_error!(span, "Invalid macro attribute. Should be plain u16, 'code = u16', 'description = str', 'schema = str' or 'default_schema = str'")
466 }
467 }
468 NestedMeta::Lit(Lit::Int(attr_value)) => {
470 let status_code = attr_value.base10_parse::<u16>()
471 .map_err(|_| emit_error!(span, "Invalid u16 in code argument")).ok();
472 list.push((status_code, None, None, attr));
473 }
474 _ => emit_error!(span, "This macro supports only named attributes - 'code' (u16), 'description' (str), 'schema' (str) or 'default_schema' (str)")
475 }
476
477 list
478 })
479 .iter()
480 .filter_map(|quad| {
482 let (code, description, schema) = match quad {
483 (Some(code), Some(description), schema, _) => {
484 (code, description.to_owned(), schema.to_owned())
485 }
486 (Some(code), None, schema, attr) => {
487 let span = attr.span().unwrap();
488 let description = StatusCode::from_u16(*code)
489 .map_err(|_| {
490 emit_warning!(span, format!("Invalid status code {}", code));
491 String::new()
492 })
493 .map(|s| s.canonical_reason()
494 .map(str::to_string)
495 .unwrap_or_else(|| {
496 emit_warning!(span, format!("Status code {} doesn't have a canonical name", code));
497 String::new()
498 })
499 )
500 .unwrap_or_else(|_| String::new());
501 (code, description, schema.to_owned())
502 }
503 (None, _, _, _) => return None,
504 };
505 Some((*code, description, schema))
506 })
507 .collect::<Vec<(u16, String, Option<syn::Ident>)>>();
508
509 let error_definitions = error_codes.iter().fold(
510 if default_schema.is_none() {
511 TokenStream2::new()
512 } else {
513 quote! {
514 #default_schema::update_definitions(map);
515 }
516 },
517 |mut stream, (_, _, schema)| {
518 if let Some(schema) = schema {
519 let tokens = quote! {
520 #schema::update_definitions(map);
521 };
522 stream.extend(tokens);
523 }
524 stream
525 },
526 );
527
528 let update_definitions = quote! {
529 fn update_definitions(map: &mut std::collections::BTreeMap<String, paperclip::v2::models::DefaultSchemaRaw>) {
530 use paperclip::actix::OperationModifier;
531 #error_definitions
532 }
533 };
534
535 let error_map = error_codes.iter().fold(
537 proc_macro2::TokenStream::new(),
538 |mut stream, (code, description, _)| {
539 let token = quote! {
540 (#code, #description),
541 };
542 stream.extend(token);
543 stream
544 },
545 );
546
547 let update_error_helper = quote! {
548 fn update_error_definitions(code: &u16, description: &str, schema: &Option<&str>, op: &mut paperclip::v2::models::DefaultOperationRaw) {
549 if let Some(schema) = &schema {
550 op.responses.insert(code.to_string(), paperclip::v2::models::Either::Right(paperclip::v2::models::Response {
551 description: Some(description.to_string()),
552 schema: Some(paperclip::v2::models::DefaultSchemaRaw {
553 name: Some(schema.to_string()),
554 reference: Some(format!("#/definitions/{}", schema)),
555 .. Default::default()
556 }),
557 ..Default::default()
558 }));
559 } else {
560 op.responses.insert(code.to_string(), paperclip::v2::models::Either::Right(paperclip::v2::models::DefaultResponseRaw {
561 description: Some(description.to_string()),
562 ..Default::default()
563 }));
564 }
565 }
566 };
567 let default_schema = default_schema.map(|i| i.to_string());
568 let update_errors = error_codes.iter().fold(
569 update_error_helper,
570 |mut stream, (code, description, schema)| {
571 let tokens = if let Some(schema) = schema {
572 let schema = schema.to_string();
573 quote! {
574 update_error_definitions(&#code, #description, &Some(#schema), op);
575 }
576 } else if let Some(scheme) = &default_schema {
577 quote! {
578 update_error_definitions(&#code, #description, &Some(#scheme), op);
579 }
580 } else {
581 quote! {
582 update_error_definitions(&#code, #description, &None, op);
583 }
584 };
585 stream.extend(tokens);
586 stream
587 },
588 );
589
590 let gen = quote! {
591 #item_ast
592
593 impl #impl_generics paperclip::v2::schema::Apiv2Errors for #name #ty_generics #where_clause {
594 const ERROR_MAP: &'static [(u16, &'static str)] = &[
595 #error_map
596 ];
597 fn update_error_definitions(op: &mut paperclip::v2::models::DefaultOperationRaw) {
598 #update_errors
599 }
600 #update_definitions
601 }
602 };
603
604 gen.into()
605}
606
607pub fn emit_v2_errors_overlay(attrs: TokenStream, input: TokenStream) -> TokenStream {
609 let item_ast = match crate::expect_struct_or_enum(input) {
610 Ok(i) => i,
611 Err(ts) => return ts,
612 };
613
614 let name = &item_ast.ident;
615 let inner = match &item_ast.data {
616 Data::Struct(s) => if s.fields.len() == 1 {
617 match &s.fields {
618 Fields::Unnamed(s) => s.unnamed.first().map(|s| match &s.ty {
619 Type::Path(s) => s.path.segments.first().map(|f| &f.ident),
620 _ => None,
621 }),
622 _ => None,
623 }
624 } else {
625 None
626 }
627 .flatten()
628 .unwrap_or_else(|| {
629 abort!(
630 s.fields.span(),
631 "This macro supports only unnamed structs with 1 element"
632 )
633 }),
634 _ => {
635 abort!(item_ast.span(), "This macro supports only unnamed structs");
636 }
637 };
638
639 let attrs = crate::parse_input_attrs(attrs);
640 let generics = item_ast.generics.clone();
641 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
642
643 let error_codes = attrs
645 .0
646 .iter()
647 .fold(Vec::new(), |mut list: Vec<u16>, attr| {
649 let span = attr.span().unwrap();
650 match attr {
651 NestedMeta::Lit(Lit::Int(attr_value)) => {
653 let status_code = attr_value
654 .base10_parse::<u16>()
655 .map_err(|_| emit_error!(span, "Invalid u16 in code argument"))
656 .unwrap();
657 list.push(status_code);
658 }
659 _ => emit_error!(
660 span,
661 "This macro supports only named attributes - 'code' (u16)"
662 ),
663 }
664
665 list
666 });
667 let filter_error_codes = error_codes
668 .iter()
669 .fold(TokenStream2::new(), |mut stream, code| {
670 let status_code = &code.to_string();
671 let tokens = quote! {
672 op.responses.remove(#status_code);
673 };
674 stream.extend(tokens);
675 stream
676 });
677
678 let gen = quote! {
679 #item_ast
680
681 impl std::fmt::Display for #name {
682 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
683 std::fmt::Display::fmt(&self.0, f)
684 }
685 }
686
687 impl actix_web::error::ResponseError for #name {
688 fn status_code(&self) -> actix_web::http::StatusCode {
689 self.0.status_code()
690 }
691 fn error_response(&self) -> actix_web::HttpResponse {
692 self.0.error_response()
693 }
694 }
695
696 impl #impl_generics paperclip::v2::schema::Apiv2Errors for #name #ty_generics #where_clause {
697 const ERROR_MAP: &'static [(u16, &'static str)] = &[];
698 fn update_definitions(map: &mut std::collections::BTreeMap<String, paperclip::v2::models::DefaultSchemaRaw>) {
699 #inner::update_definitions(map);
700 }
701 fn update_error_definitions(op: &mut paperclip::v2::models::DefaultOperationRaw) {
702 #inner::update_error_definitions(op);
703 #filter_error_codes
704 }
705 }
706 };
707
708 gen.into()
709}
710
711fn extract_rename(attrs: &[Attribute]) -> Option<String> {
712 let attrs = extract_openapi_attrs(attrs);
713 for attr in attrs.flat_map(|attr| attr.into_iter()) {
714 if let NestedMeta::Meta(Meta::NameValue(nv)) = attr {
715 if nv.path.is_ident("rename") {
716 if let Lit::Str(s) = nv.lit {
717 return Some(s.value());
718 } else {
719 emit_error!(
720 nv.lit.span().unwrap(),
721 format!(
722 "`#[{}(rename = \"...\")]` expects a string argument",
723 SCHEMA_MACRO_ATTR
724 ),
725 );
726 }
727 }
728 }
729 }
730
731 None
732}
733
734fn extract_example(attrs: &[Attribute]) -> Option<String> {
735 let attrs = extract_openapi_attrs(attrs);
736 for attr in attrs.flat_map(|attr| attr.into_iter()) {
737 if let NestedMeta::Meta(Meta::NameValue(nv)) = attr {
738 if nv.path.is_ident("example") {
739 if let Lit::Str(s) = nv.lit {
740 return Some(s.value());
741 } else {
742 emit_error!(
743 nv.lit.span().unwrap(),
744 format!(
745 "`#[{}(example = \"...\")]` expects a string argument",
746 SCHEMA_MACRO_ATTR
747 ),
748 );
749 }
750 }
751 }
752 }
753
754 None
755}
756
757fn field_extract_f32(nv: MetaNameValue) -> Option<proc_macro2::TokenStream> {
758 let value: Result<proc_macro2::TokenStream, String> = match &nv.lit {
759 Lit::Str(s) => match s.value().parse::<f32>() {
760 Ok(s) => Ok(quote! { #s }),
761 Err(error) => Err(error.to_string()),
762 },
763 Lit::Float(f) => Ok(quote! { #f }),
764 Lit::Int(i) => {
765 let f: f32 = i
766 .base10_parse()
767 .unwrap_or_else(|e| abort!(i.span(), "{}", e));
768 Ok(quote! { #f })
769 }
770 _ => {
771 emit_error!(
772 nv.lit.span().unwrap(),
773 "Expected a string, float or int argument"
774 );
775 return None;
776 }
777 };
778 match value {
779 Ok(value) => Some(value),
780 Err(error) => {
781 emit_error!(nv.lit.span().unwrap(), error);
782 None
783 }
784 }
785}
786
787fn extract_openapi_f32(attrs: &[Attribute], ident: &str) -> Option<proc_macro2::TokenStream> {
788 let attrs = extract_openapi_attrs(attrs);
789 for attr in attrs.flat_map(|attr| attr.into_iter()) {
790 if let NestedMeta::Meta(Meta::NameValue(nv)) = attr {
791 if nv.path.is_ident(ident) {
792 return field_extract_f32(nv);
793 }
794 }
795 }
796
797 None
798}
799
800pub fn emit_v2_definition(input: TokenStream) -> TokenStream {
802 let item_ast = match crate::expect_struct_or_enum(input) {
803 Ok(i) => i,
804 Err(ts) => return ts,
805 };
806
807 if let Some(empty) = check_empty_schema(&item_ast) {
808 return empty;
809 }
810
811 let docs = extract_documentation(&item_ast.attrs);
812 let docs = docs.trim();
813
814 let example = if let Some(example) = extract_example(&item_ast.attrs) {
815 quote!(
817 paperclip::v2::serde_json::from_str::<paperclip::v2::serde_json::Value>(#example).ok().or_else(|| Some(#example.into()))
818 )
819 } else {
820 quote!(None)
821 };
822
823 let props = SerdeProps::from_item_attrs(&item_ast.attrs);
824
825 let name = &item_ast.ident;
826
827 let mut generics = item_ast.generics.clone();
829 let bound = syn::parse2::<TraitBound>(quote!(paperclip::v2::schema::Apiv2Schema))
830 .expect("expected to parse trait bound");
831 generics.type_params_mut().for_each(|param| {
832 param.bounds.push(bound.clone().into());
833 });
834
835 let opt_impl = add_optional_impl(name, &generics);
836 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
837
838 let mut props_gen = quote! {};
840
841 match &item_ast.data {
842 Data::Struct(ref s) => {
843 props_gen.extend(quote!(
844 schema.data_type = Some(DataType::Object);
845 ));
846 match &s.fields {
847 Fields::Named(ref f) => {
848 handle_field_struct(f, &item_ast.attrs, &props, &mut props_gen)
849 }
850 Fields::Unnamed(ref f) => {
851 handle_unnamed_field_struct(f, &item_ast.attrs, &mut props_gen)
852 }
853 Fields::Unit => {
854 emit_warning!(
855 s.struct_token.span().unwrap(),
856 "unit structs do not have any fields and hence will have empty schema.";
857 help = "{}", &*EMPTY_SCHEMA_HELP;
858 );
859 }
860 }
861 }
862 Data::Enum(ref e) => handle_enum(e, &props, &mut props_gen),
863 Data::Union(ref u) => emit_error!(
864 u.union_token.span().unwrap(),
865 "unions are unsupported for deriving schema"
866 ),
867 };
868
869 let base_name = extract_rename(&item_ast.attrs).unwrap_or_else(|| name.to_string());
870 let type_params: Vec<&Ident> = generics.type_params().map(|p| &p.ident).collect();
871 let schema_name = if type_params.is_empty() {
872 quote! { #base_name }
873 } else {
874 let type_names = quote! {
875 [#(#type_params::name()),*]
876 .iter()
877 .filter_map(|n| n.to_owned())
878 .collect::<Vec<String>>()
879 .join(", ")
880 };
881 quote! { format!("{}<{}>", #base_name, #type_names) }
882 };
883 let props_gen_empty = props_gen.is_empty();
884
885 #[cfg(not(feature = "path-in-definition"))]
886 let default_schema_raw_def = quote! {
887 let mut schema = DefaultSchemaRaw {
888 name: Some(#schema_name.into()),
889 example: #example,
890 ..Default::default()
891 };
892 };
893
894 #[cfg(feature = "path-in-definition")]
895 let default_schema_raw_def = quote! {
896 let mut schema = DefaultSchemaRaw {
897 name: Some(Self::__paperclip_schema_name()), example: #example,
899 .. Default::default()
900 };
901 };
902
903 #[cfg(not(feature = "path-in-definition"))]
904 let paperclip_schema_name_def = quote!();
905
906 #[cfg(feature = "path-in-definition")]
907 let paperclip_schema_name_def = quote! {
908 fn __paperclip_schema_name() -> String {
909 let full_module_path = std::module_path!().to_string();
911 let trimmed_module_path = full_module_path.split("::")
913 .enumerate()
914 .filter(|(index, _)| *index != 0) .map(|(_, component)| component)
916 .collect::<Vec<_>>()
917 .join("_");
918 format!("{}_{}", trimmed_module_path, #schema_name)
919 }
920 };
921
922 #[cfg(not(feature = "path-in-definition"))]
923 let const_name_def = quote! {
924 fn name() -> Option<String> {
925 Some(#schema_name.to_string())
926 }
927 };
928
929 #[cfg(feature = "path-in-definition")]
930 let const_name_def = quote!();
931
932 #[cfg(not(feature = "path-in-definition"))]
933 let props_gen_empty_name_def = quote! {
934 schema.name = Some(#schema_name.into());
935 };
936
937 #[cfg(feature = "path-in-definition")]
938 let props_gen_empty_name_def = quote! {
939 schema.name = Some(Self::__paperclip_schema_name());
940 };
941
942 let gen = quote! {
943 impl #impl_generics #name #ty_generics #where_clause {
944 #paperclip_schema_name_def
945 }
946
947 impl #impl_generics paperclip::v2::schema::Apiv2Schema for #name #ty_generics #where_clause {
948 #const_name_def
949 fn description() -> &'static str {
950 #docs
951 }
952
953 fn raw_schema() -> paperclip::v2::models::DefaultSchemaRaw {
954 use paperclip::v2::models::{DataType, DataTypeFormat, DefaultSchemaRaw};
955 use paperclip::v2::schema::TypedData;
956
957 #default_schema_raw_def
958
959 #props_gen
960 if !#props_gen_empty {
964 #props_gen_empty_name_def
965 }
966 schema
967 }
968 }
969
970 #opt_impl
971 };
972
973 gen.into()
974}
975
976pub fn emit_v2_security(input: TokenStream) -> TokenStream {
978 let item_ast = match crate::expect_struct_or_enum(input) {
979 Ok(i) => i,
980 Err(ts) => return ts,
981 };
982
983 if let Some(empty) = check_empty_schema(&item_ast) {
984 return empty;
985 }
986
987 let name = &item_ast.ident;
988 let mut generics = item_ast.generics.clone();
990 let bound = syn::parse2::<TraitBound>(quote!(paperclip::v2::schema::Apiv2Schema))
991 .expect("expected to parse trait bound");
992 generics.type_params_mut().for_each(|param| {
993 param.bounds.push(bound.clone().into());
994 });
995
996 let opt_impl = add_optional_impl(name, &generics);
997 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
998
999 let mut security_attrs = HashMap::new();
1000 let mut scopes = Vec::new();
1001
1002 let valid_attrs = vec![
1003 "alias",
1004 "description",
1005 "name",
1006 "in",
1007 "flow",
1008 "auth_url",
1009 "token_url",
1010 "parent",
1011 ];
1012 let invalid_attr_msg = format!("Invalid macro attribute. Should be bare security type [\"apiKey\", \"oauth2\"] or named attribute {:?}", valid_attrs);
1013
1014 for nested in extract_openapi_attrs(&item_ast.attrs) {
1016 for nested_attr in nested {
1017 let span = nested_attr.span().unwrap();
1018 match &nested_attr {
1019 NestedMeta::Meta(Meta::Path(attr_path)) => {
1021 if let Some(type_) = attr_path.get_ident() {
1022 if security_attrs
1023 .insert("type".to_string(), type_.to_string())
1024 .is_some()
1025 {
1026 emit_warning!(span, "Auth type defined multiple times.");
1027 }
1028 }
1029 }
1030 NestedMeta::Meta(Meta::NameValue(name_value)) => {
1032 let attr_name = name_value.path.get_ident().map(|id| id.to_string());
1033 let attr_value = &name_value.lit;
1034
1035 if let Some(attr_name) = attr_name {
1036 if valid_attrs.contains(&attr_name.as_str()) {
1037 if let Lit::Str(attr_value) = attr_value {
1038 if security_attrs
1039 .insert(attr_name.clone(), attr_value.value())
1040 .is_some()
1041 {
1042 emit_warning!(
1043 span,
1044 "Attribute {} defined multiple times.",
1045 attr_name
1046 );
1047 }
1048 } else {
1049 emit_warning!(
1050 span,
1051 "Invalid value for named attribute: {}",
1052 attr_name
1053 );
1054 }
1055 } else {
1056 emit_warning!(span, invalid_attr_msg);
1057 }
1058 } else {
1059 emit_error!(span, invalid_attr_msg);
1060 }
1061 }
1062 NestedMeta::Meta(Meta::List(list_attr)) => {
1064 match list_attr
1065 .path
1066 .get_ident()
1067 .map(|id| id.to_string())
1068 .as_deref()
1069 {
1070 Some("scopes") => {
1071 for nested in &list_attr.nested {
1072 match nested {
1073 NestedMeta::Lit(Lit::Str(value)) => {
1074 scopes.push(value.value().to_string())
1075 }
1076 _ => emit_error!(
1077 nested.span().unwrap(),
1078 "Invalid list attribute value"
1079 ),
1080 }
1081 }
1082 }
1083 Some(path) => emit_error!(span, "Invalid list attribute: {}", path),
1084 _ => emit_error!(span, "Invalid list attribute"),
1085 }
1086 }
1087 _ => {
1088 emit_error!(span, invalid_attr_msg);
1089 }
1090 }
1091 }
1092 }
1093
1094 let scopes_stream = scopes
1095 .iter()
1096 .fold(proc_macro2::TokenStream::new(), |mut stream, scope| {
1097 stream.extend(quote! {
1098 oauth2_scopes.insert(#scope.to_string(), #scope.to_string());
1099 });
1100 stream
1101 });
1102
1103 let (security_def, security_def_name) = match (
1104 security_attrs.get("type"),
1105 security_attrs.get("parent"),
1106 ) {
1107 (Some(type_), None) => {
1108 let alias = security_attrs.get("alias").unwrap_or(type_);
1109 let quoted_description = quote_option(security_attrs.get("description"));
1110 let quoted_name = quote_option(security_attrs.get("name"));
1111 let quoted_in = quote_option(security_attrs.get("in"));
1112 let quoted_flow = quote_option(security_attrs.get("flow"));
1113 let quoted_auth_url = quote_option(security_attrs.get("auth_url"));
1114 let quoted_token_url = quote_option(security_attrs.get("token_url"));
1115
1116 (
1117 Some(quote! {
1118 Some(paperclip::v2::models::SecurityScheme {
1119 type_: #type_.to_string(),
1120 name: #quoted_name,
1121 in_: #quoted_in,
1122 flow: #quoted_flow,
1123 auth_url: #quoted_auth_url,
1124 token_url: #quoted_token_url,
1125 scopes: std::collections::BTreeMap::new(),
1126 description: #quoted_description,
1127 })
1128 }),
1129 Some(quote!(Some(#alias.to_string()))),
1130 )
1131 }
1132 (None, Some(parent)) => {
1133 let parent_ident = Ident::new(parent, proc_macro2::Span::call_site());
1134 (
1136 Some(quote! {
1137 let mut oauth2_scopes = std::collections::BTreeMap::new();
1138 #scopes_stream
1139 let mut scheme = #parent_ident::security_scheme()
1140 .expect("empty schema. did you derive `Apiv2Security` for parent struct?");
1141 scheme.scopes = oauth2_scopes;
1142 Some(scheme)
1143 }),
1144 Some(quote!(<#parent_ident as paperclip::v2::schema::Apiv2Schema>::name())),
1145 )
1146 }
1147 (Some(_), Some(_)) => {
1148 emit_error!(
1149 item_ast.span().unwrap(),
1150 "Can't define new security type and use parent attribute together."
1151 );
1152 (None, None)
1153 }
1154 (None, None) => {
1155 emit_error!(
1156 item_ast.span().unwrap(),
1157 "Invalid attributes. Expected security type or parent defined."
1158 );
1159 (None, None)
1160 }
1161 };
1162
1163 let gen = if let (Some(def_block), Some(def_name)) = (security_def, security_def_name) {
1164 quote! {
1165 impl #impl_generics paperclip::v2::schema::Apiv2Schema for #name #ty_generics #where_clause {
1166 fn name() -> Option<String> {
1167 #def_name
1168 }
1169
1170 fn security_scheme() -> Option<paperclip::v2::models::SecurityScheme> {
1171 #def_block
1172 }
1173 }
1174
1175 #opt_impl
1176 }
1177 } else {
1178 quote! {}
1179 };
1180
1181 gen.into()
1182}
1183
1184pub fn emit_v2_header(input: TokenStream) -> TokenStream {
1186 let item_ast = match crate::expect_struct_or_enum(input) {
1187 Ok(i) => i,
1188 Err(ts) => return ts,
1189 };
1190
1191 if let Some(empty) = check_empty_schema(&item_ast) {
1192 return empty;
1193 }
1194
1195 let name = &item_ast.ident;
1196 let mut generics = item_ast.generics.clone();
1198 let bound = syn::parse2::<TraitBound>(quote!(paperclip::v2::schema::Apiv2Schema))
1199 .expect("expected to parse trait bound");
1200 generics.type_params_mut().for_each(|param| {
1201 param.bounds.push(bound.clone().into());
1202 });
1203
1204 let opt_impl = add_optional_impl(name, &generics);
1205 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
1206
1207 let mut header_definitions = vec![];
1208
1209 let valid_attrs = vec!["description", "name", "format", "maximum", "minimum"];
1210 let invalid_attr_msg = format!(
1211 "Invalid macro attribute. Should be named attribute {:?}",
1212 valid_attrs
1213 );
1214
1215 fn quote_format(format: &str) -> proc_macro2::TokenStream {
1216 match format {
1217 "int32" => quote! { Some(paperclip::v2::models::DataTypeFormat::Int32) },
1218 "int64" => quote! { Some(paperclip::v2::models::DataTypeFormat::Int64) },
1219 "float" => quote! { Some(paperclip::v2::models::DataTypeFormat::Float) },
1220 "double" => quote! { Some(paperclip::v2::models::DataTypeFormat::Double) },
1221 "byte" => quote! { Some(paperclip::v2::models::DataTypeFormat::Byte) },
1222 "binary" => quote! { Some(paperclip::v2::models::DataTypeFormat::Binary) },
1223 "date" => quote! { Some(paperclip::v2::models::DataTypeFormat::Date) },
1224 "datetime" | "date-time" => {
1225 quote! { Some(paperclip::v2::models::DataTypeFormat::DateTime) }
1226 }
1227 "password" => quote! { Some(paperclip::v2::models::DataTypeFormat::Password) },
1228 "url" => quote! { Some(paperclip::v2::models::DataTypeFormat::Url) },
1229 "uuid" => quote! { Some(paperclip::v2::models::DataTypeFormat::Uuid) },
1230 "ip" => quote! { Some(paperclip::v2::models::DataTypeFormat::Ip) },
1231 "ipv4" => quote! { Some(paperclip::v2::models::DataTypeFormat::IpV4) },
1232 "ipv6" => quote! { Some(paperclip::v2::models::DataTypeFormat::IpV6) },
1233 "other" => quote! { Some(paperclip::v2::models::DataTypeFormat::Other) },
1234 v => {
1235 emit_error!(
1236 format.span().unwrap(),
1237 format!("Invalid format attribute value. Got {}", v)
1238 );
1239 quote! { None }
1240 }
1241 }
1242 }
1243
1244 let struct_ast = match &item_ast.data {
1245 Data::Struct(struct_ast) => struct_ast,
1246 Data::Enum(_) | Data::Union(_) => {
1247 emit_error!(
1248 item_ast.span(),
1249 "Invalid data type. Apiv2Header should be defined on a struct"
1250 );
1251 return quote!().into();
1252 }
1253 };
1254
1255 if extract_openapi_attrs(&item_ast.attrs)
1256 .peekable()
1257 .peek()
1258 .is_some()
1259 {
1260 emit_error!(
1261 item_ast.span(),
1262 "Invalid openapi attribute. openapi attribute should be defined at struct fields level"
1263 );
1264 return quote!().into();
1265 }
1266
1267 for field in &struct_ast.fields {
1268 let mut parameter_attrs = HashMap::new();
1269 let field_name = &field.ident;
1270 let docs = extract_documentation(&field.attrs);
1271 let docs = docs.trim();
1272
1273 for nested in extract_openapi_attrs(&field.attrs) {
1275 for nested_attr in nested {
1276 let span = nested_attr.span().unwrap();
1277 match &nested_attr {
1278 NestedMeta::Meta(Meta::Path(attr_path)) => {
1280 if let Some(attr) = attr_path.get_ident() {
1281 if *attr == "skip" {
1282 parameter_attrs.insert("skip".to_owned(), "".to_owned());
1283 }
1284 }
1285 }
1286 NestedMeta::Meta(Meta::NameValue(name_value)) => {
1288 let attr_name = name_value.path.get_ident().map(|id| id.to_string());
1289 let attr_value = &name_value.lit;
1290
1291 if let Some(attr_name) = attr_name {
1292 if valid_attrs.contains(&attr_name.as_str()) {
1293 if let Some(value) = match attr_value {
1294 Lit::Str(attr_value) => Some(attr_value.value()),
1295 Lit::Float(x) => Some(x.to_string()),
1296 Lit::Int(x) => Some(x.to_string()),
1297 _ => {
1298 emit_warning!(
1299 span,
1300 "Invalid value for named attribute: {}",
1301 attr_name
1302 );
1303 None
1304 }
1305 } {
1306 if parameter_attrs.insert(attr_name.clone(), value).is_some() {
1307 emit_warning!(
1308 span,
1309 "Attribute {} defined multiple times.",
1310 attr_name
1311 );
1312 }
1313 }
1314 } else {
1315 emit_warning!(span, invalid_attr_msg);
1316 }
1317 } else {
1318 emit_error!(span, invalid_attr_msg);
1319 }
1320 }
1321 _ => {
1322 emit_error!(span, invalid_attr_msg);
1323 }
1324 }
1325 }
1326 }
1327
1328 if parameter_attrs.contains_key("skip") {
1329 continue;
1330 }
1331
1332 let docs = (!docs.is_empty()).then(|| docs.to_owned());
1333 let quoted_description = quote_option(parameter_attrs.get("description").or(docs.as_ref()));
1334 let name_string = field_name.as_ref().map(|name| name.to_string());
1335 let quoted_name = if let Some(name) = parameter_attrs.get("name").or(name_string.as_ref()) {
1336 name
1337 } else {
1338 emit_error!(
1339 field.span(),
1340 "Missing header name. Either add a name using the openapi attribute or use named struct parameter"
1341 );
1342 return quote!().into();
1343 };
1344
1345 let (quoted_type, quoted_format) = if let Some(ty_ref) = get_field_type(field) {
1346 (
1347 quote! { {
1348 use paperclip::v2::schema::TypedData;
1349 Some(#ty_ref::data_type())
1350 } },
1351 quote! { {
1352 use paperclip::v2::schema::TypedData;
1353 #ty_ref::format()
1354 } },
1355 )
1356 } else {
1357 (quote! { None }, quote! { None })
1358 };
1359
1360 let (quoted_type, quoted_format) = if let Some(format) = parameter_attrs.get("format") {
1361 let quoted_format = quote_format(format);
1362 let quoted_type = quote! { #quoted_format.map(|format| format.into()) };
1363 (quoted_type, quoted_format)
1364 } else {
1365 (quoted_type, quoted_format)
1366 };
1367
1368 let quoted_max = quote_option_str_f32(field, parameter_attrs.get("maximum"));
1369 let quoted_min = quote_option_str_f32(field, parameter_attrs.get("minimum"));
1370
1371 let def_block = quote! {
1372 paperclip::v2::models::Parameter::<paperclip::v2::models::DefaultSchemaRaw> {
1373 name: #quoted_name.to_owned(),
1374 in_: paperclip::v2::models::ParameterIn::Header,
1375 description: #quoted_description,
1376 data_type: #quoted_type,
1377 format: #quoted_format,
1378 maximum: #quoted_max,
1379 minimum: #quoted_min,
1380 required: Self::required(),
1381 ..Default::default()
1382 }
1383 };
1384
1385 header_definitions.push(def_block);
1386 }
1387
1388 let gen = quote! {
1389 impl #impl_generics paperclip::v2::schema::Apiv2Schema for #name #ty_generics #where_clause {
1390 fn header_parameter_schema() -> Vec<paperclip::v2::models::Parameter<paperclip::v2::models::DefaultSchemaRaw>> {
1391 vec![
1392 #(#header_definitions),*
1393 ]
1394 }
1395 }
1396
1397 #opt_impl
1398 };
1399
1400 gen.into()
1401}
1402
1403fn quote_option(value: Option<&String>) -> proc_macro2::TokenStream {
1404 if let Some(value) = value {
1405 quote! { Some(#value.to_string()) }
1406 } else {
1407 quote! { None }
1408 }
1409}
1410fn quote_option_str_f32(field: &Field, value: Option<&String>) -> proc_macro2::TokenStream {
1411 if let Some(x) = value {
1412 let x: f32 = match x.parse() {
1413 Ok(x) => x,
1414 Err(error) => {
1415 emit_error!(field.span(), error.to_string());
1416 0.0
1417 }
1418 };
1419 quote! { Some(#x) }
1420 } else {
1421 quote! { None }
1422 }
1423}
1424
1425#[cfg(feature = "nightly")]
1426fn add_optional_impl(_: &Ident, _: &Generics) -> proc_macro2::TokenStream {
1427 quote!()
1429}
1430
1431#[cfg(not(feature = "nightly"))]
1432fn add_optional_impl(name: &Ident, generics: &Generics) -> proc_macro2::TokenStream {
1433 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
1434 quote! {
1435 impl #impl_generics paperclip::actix::OperationModifier for #name #ty_generics #where_clause {}
1436 }
1437}
1438
1439fn get_field_type(field: &Field) -> Option<proc_macro2::TokenStream> {
1440 match field.ty {
1441 Type::Path(_) | Type::Reference(_) | Type::Array(_) => {
1442 Some(address_type_for_fn_call(&field.ty))
1443 }
1444 _ => {
1445 emit_warning!(
1446 field.ty.span().unwrap(),
1447 "unsupported field type will be ignored."
1448 );
1449 None
1450 }
1451 }
1452}
1453
1454fn handle_unnamed_field_struct(
1456 fields: &FieldsUnnamed,
1457 struct_attr: &[Attribute],
1458 props_gen: &mut proc_macro2::TokenStream,
1459) {
1460 if fields.unnamed.len() == 1 {
1461 let field = fields.unnamed.iter().next().unwrap();
1462
1463 if let Some(ty_ref) = get_field_type(field) {
1464 let docs = extract_documentation(struct_attr);
1465 let docs = docs.trim();
1466
1467 if SerdeSkip::exists(&field.attrs) {
1468 props_gen.extend(quote!({
1469 let mut s: DefaultSchemaRaw = Default::default();
1470 if !#docs.is_empty() {
1471 s.description = Some(#docs.to_string());
1472 }
1473 schema = s;
1474 }));
1475 } else {
1476 props_gen.extend(quote!({
1477 let mut s = #ty_ref::raw_schema();
1478 if !#docs.is_empty() {
1479 s.description = Some(#docs.to_string());
1480 }
1481 schema = s;
1482 }));
1483 }
1484 }
1485 } else {
1486 for (inner_field_id, field) in fields.unnamed.iter().enumerate() {
1487 if SerdeSkip::exists(&field.attrs) {
1488 continue;
1489 }
1490
1491 let ty_ref = match get_field_type(field) {
1492 Some(ty_ref) => ty_ref,
1493 None => continue,
1494 };
1495
1496 let docs = extract_documentation(&field.attrs);
1497 let docs = docs.trim();
1498
1499 let gen = if !SerdeFlatten::exists(&field.attrs) {
1500 quote!({
1504 let mut s = #ty_ref::raw_schema();
1505 if !#docs.is_empty() {
1506 s.description = Some(#docs.to_string());
1507 }
1508 schema.properties.insert(#inner_field_id.to_string(), s.into());
1509 if #ty_ref::required() {
1510 schema.required.insert(#inner_field_id.to_string());
1511 }
1512 })
1513 } else {
1514 quote!({
1515 let s = #ty_ref::raw_schema();
1516 schema.properties.extend(s.properties);
1517
1518 if #ty_ref::required() {
1519 schema.required.extend(s.required);
1520 }
1521 })
1522 };
1523
1524 props_gen.extend(gen);
1525 }
1526 }
1527}
1528
1529fn extract_openapi_attrs(
1531 field_attrs: &'_ [Attribute],
1532) -> impl Iterator<Item = Punctuated<syn::NestedMeta, syn::token::Comma>> + '_ {
1533 field_attrs.iter().filter_map(|a| match a.parse_meta() {
1534 Ok(Meta::List(list)) if list.path.is_ident(SCHEMA_MACRO_ATTR) => Some(list.nested),
1535 _ => None,
1536 })
1537}
1538
1539fn extract_deprecated(attrs: &[Attribute]) -> bool {
1540 attrs.iter().any(|a| match a.parse_meta() {
1541 Ok(Meta::Path(mp)) if mp.is_ident("deprecated") => true,
1542 Ok(Meta::List(mml)) => mml
1543 .path
1544 .segments
1545 .into_iter()
1546 .any(|p| p.ident == "deprecated"),
1547 _ => false,
1548 })
1549}
1550
1551fn extract_documentation(attrs: &[Attribute]) -> String {
1553 attrs
1554 .iter()
1555 .filter_map(|a| match a.parse_meta() {
1556 Ok(Meta::NameValue(mnv)) if mnv.path.is_ident("doc") => match &mnv.lit {
1557 Lit::Str(s) => Some(s.value()),
1558 _ => None,
1559 },
1560 _ => None,
1561 })
1562 .collect::<Vec<String>>()
1563 .join("\n")
1564}
1565
1566fn check_empty_schema(item_ast: &DeriveInput) -> Option<TokenStream> {
1568 let needs_empty_schema = extract_openapi_attrs(&item_ast.attrs).any(|nested| {
1569 nested.len() == 1
1570 && match &nested[0] {
1571 NestedMeta::Meta(Meta::Path(path)) => path.is_ident("empty"),
1572 _ => false,
1573 }
1574 });
1575
1576 if needs_empty_schema {
1577 let name = &item_ast.ident;
1578 let generics = item_ast.generics.clone();
1579 let opt_impl = add_optional_impl(name, &generics);
1580 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
1581 return Some(quote!(
1582 impl #impl_generics paperclip::v2::schema::Apiv2Schema for #name #ty_generics #where_clause {}
1583
1584 #opt_impl
1585 ).into());
1586 }
1587
1588 None
1589}
1590
1591fn handle_field_struct(
1593 fields: &FieldsNamed,
1594 struct_attr: &[Attribute],
1595 serde: &SerdeProps,
1596 props_gen: &mut proc_macro2::TokenStream,
1597) {
1598 let docs = extract_documentation(struct_attr);
1599 let docs = docs.trim();
1600
1601 props_gen.extend(quote!({
1602 if !#docs.is_empty() {
1603 schema.description = Some(#docs.to_string());
1604 }
1605 }));
1606 for field in &fields.named {
1607 let mut field_name = field
1608 .ident
1609 .as_ref()
1610 .expect("missing field name?")
1611 .to_string();
1612
1613 field_name = field_name
1615 .strip_prefix("r#")
1616 .map(|n| n.to_string())
1617 .unwrap_or(field_name);
1618
1619 if SerdeSkip::exists(&field.attrs) {
1620 continue;
1621 }
1622
1623 if let Some(renamed) = SerdeRename::from_field_attrs(&field.attrs) {
1624 field_name = renamed;
1625 } else if let Some(prop) = serde.rename {
1626 field_name = prop.rename(&field_name);
1627 }
1628
1629 let ty_ref = match get_field_type(field) {
1630 Some(ty_ref) => ty_ref,
1631 None => continue,
1632 };
1633
1634 let docs = extract_documentation(&field.attrs);
1635 let docs = docs.trim();
1636
1637 let example = if let Some(example) = extract_example(&field.attrs) {
1638 quote!({
1640 s.example = paperclip::v2::serde_json::from_str::<paperclip::v2::serde_json::Value>(#example).ok().or_else(|| Some(#example.into()));
1641 })
1642 } else {
1643 quote!({})
1644 };
1645
1646 let max = if let Some(max) = extract_openapi_f32(&field.attrs, "maximum") {
1647 quote!({
1648 s.maximum = Some(#max);
1649 })
1650 } else {
1651 quote!({})
1652 };
1653 let min = if let Some(min) = extract_openapi_f32(&field.attrs, "minimum") {
1654 quote!({
1655 s.minimum = Some(#min);
1656 })
1657 } else {
1658 quote!({})
1659 };
1660
1661 let gen = if !SerdeFlatten::exists(&field.attrs) {
1662 quote!({
1663 let mut s = #ty_ref::raw_schema();
1664 if !#docs.is_empty() {
1665 s.description = Some(#docs.to_string());
1666 }
1667 #example;
1668 #max;
1669 #min;
1670 schema.properties.insert(#field_name.into(), s.into());
1671
1672 if #ty_ref::required() {
1673 schema.required.insert(#field_name.into());
1674 }
1675 })
1676 } else {
1677 quote!({
1678 let s = #ty_ref::raw_schema();
1679 schema.properties.extend(s.properties);
1680
1681 if #ty_ref::required() {
1682 schema.required.extend(s.required);
1683 }
1684 })
1685 };
1686
1687 props_gen.extend(gen);
1688 }
1689}
1690
1691fn handle_enum(e: &DataEnum, serde: &SerdeProps, props_gen: &mut proc_macro2::TokenStream) {
1693 props_gen.extend(quote!(
1694 schema.data_type = Some(DataType::String);
1695 ));
1696
1697 for var in &e.variants {
1698 let mut name = var.ident.to_string();
1699 match &var.fields {
1700 Fields::Unit => (),
1701 Fields::Named(ref f) => {
1702 emit_warning!(
1703 f.span().unwrap(),
1704 "skipping enum variant with named fields in schema."
1705 );
1706 continue;
1707 }
1708 Fields::Unnamed(ref f) => {
1709 emit_warning!(f.span().unwrap(), "skipping tuple enum variant in schema.");
1710 continue;
1711 }
1712 }
1713
1714 if SerdeSkip::exists(&var.attrs) {
1715 continue;
1716 }
1717
1718 if let Some(renamed) = SerdeRename::from_field_attrs(&var.attrs) {
1719 name = renamed;
1720 } else if let Some(prop) = serde.rename {
1721 name = prop.rename(&name);
1722 }
1723
1724 props_gen.extend(quote!(
1725 schema.enum_.push(paperclip::v2::serde_json::json!(#name));
1726 ));
1727 }
1728}
1729
1730fn address_type_for_fn_call(old_ty: &Type) -> proc_macro2::TokenStream {
1735 if matches!(old_ty, Type::Reference(_) | Type::Array(_)) {
1736 return quote!(<(#old_ty)>);
1737 }
1738
1739 let mut ty = old_ty.clone();
1740 if let Type::Path(ref mut p) = &mut ty {
1741 p.path.segments.pairs_mut().for_each(|mut pair| {
1742 let is_empty = pair.value().arguments.is_empty();
1743 let args = &mut pair.value_mut().arguments;
1744 match args {
1745 PathArguments::AngleBracketed(ref mut brack_args) if !is_empty => {
1746 brack_args.colon2_token = Some(Token));
1747 }
1748 _ => (),
1749 }
1750 });
1751 }
1752
1753 quote!(#ty)
1754}
1755
1756#[derive(Clone, Copy, Debug, Eq, PartialEq, EnumString)]
1760enum SerdeRename {
1761 #[strum(serialize = "lowercase")]
1762 Lower,
1763 #[strum(serialize = "UPPERCASE")]
1764 Upper,
1765 #[strum(serialize = "PascalCase")]
1766 Pascal,
1767 #[strum(serialize = "camelCase")]
1768 Camel,
1769 #[strum(serialize = "snake_case")]
1770 Snake,
1771 #[strum(serialize = "SCREAMING_SNAKE_CASE")]
1772 ScreamingSnake,
1773 #[strum(serialize = "kebab-case")]
1774 Kebab,
1775 #[strum(serialize = "SCREAMING-KEBAB-CASE")]
1776 ScreamingKebab,
1777}
1778
1779impl SerdeRename {
1780 fn from_field_attrs(field_attrs: &[Attribute]) -> Option<String> {
1783 for meta in field_attrs.iter().filter_map(|a| a.parse_meta().ok()) {
1784 let inner_meta = match meta {
1785 Meta::List(ref l)
1786 if l.path
1787 .segments
1788 .last()
1789 .map(|p| p.ident == "serde")
1790 .unwrap_or(false) =>
1791 {
1792 &l.nested
1793 }
1794 _ => continue,
1795 };
1796
1797 for meta in inner_meta {
1798 let rename = match meta {
1799 NestedMeta::Meta(Meta::NameValue(ref v))
1800 if v.path
1801 .segments
1802 .last()
1803 .map(|p| p.ident == "rename")
1804 .unwrap_or(false) =>
1805 {
1806 &v.lit
1807 }
1808 _ => continue,
1809 };
1810
1811 if let Lit::Str(ref s) = rename {
1812 return Some(s.value());
1813 }
1814 }
1815 }
1816
1817 None
1818 }
1819
1820 fn rename(self, name: &str) -> String {
1822 match self {
1823 SerdeRename::Lower => name.to_lowercase(),
1824 SerdeRename::Upper => name.to_uppercase(),
1825 SerdeRename::Pascal => name.to_pascal_case(),
1826 SerdeRename::Camel => name.to_lower_camel_case(),
1827 SerdeRename::Snake => name.to_snake_case(),
1828 SerdeRename::ScreamingSnake => name.to_shouty_snake_case(),
1829 SerdeRename::Kebab => name.to_kebab_case(),
1830 SerdeRename::ScreamingKebab => name.to_shouty_kebab_case(),
1831 }
1832 }
1833}
1834
1835struct SerdeSkip;
1840
1841impl SerdeSkip {
1842 fn exists(field_attrs: &[Attribute]) -> bool {
1845 for meta in field_attrs.iter().filter_map(|a| a.parse_meta().ok()) {
1846 let inner_meta = match meta {
1847 Meta::List(ref l)
1848 if l.path
1849 .segments
1850 .last()
1851 .map(|p| p.ident == "serde")
1852 .unwrap_or(false) =>
1853 {
1854 &l.nested
1855 }
1856 _ => continue,
1857 };
1858 for meta in inner_meta {
1859 if let NestedMeta::Meta(Meta::Path(path)) = meta {
1860 if path.segments.iter().any(|s| s.ident == "skip") {
1861 return true;
1862 }
1863 }
1864 }
1865 }
1866
1867 false
1868 }
1869}
1870
1871#[derive(Clone, Debug, Default)]
1872struct SerdeProps {
1873 rename: Option<SerdeRename>,
1874}
1875
1876impl SerdeProps {
1877 fn from_item_attrs(item_attrs: &[Attribute]) -> Self {
1880 let mut props = Self::default();
1881 for meta in item_attrs.iter().filter_map(|a| a.parse_meta().ok()) {
1882 let inner_meta = match meta {
1883 Meta::List(ref l)
1884 if l.path
1885 .segments
1886 .last()
1887 .map(|p| p.ident == "serde")
1888 .unwrap_or(false) =>
1889 {
1890 &l.nested
1891 }
1892 _ => continue,
1893 };
1894
1895 for meta in inner_meta {
1896 let global_rename = match meta {
1897 NestedMeta::Meta(Meta::NameValue(ref v))
1898 if v.path
1899 .segments
1900 .last()
1901 .map(|p| p.ident == "rename_all")
1902 .unwrap_or(false) =>
1903 {
1904 &v.lit
1905 }
1906 _ => continue,
1907 };
1908
1909 if let Lit::Str(ref s) = global_rename {
1910 props.rename = s.value().parse().ok();
1911 }
1912 }
1913 }
1914
1915 props
1916 }
1917}
1918
1919struct SerdeFlatten;
1921
1922impl SerdeFlatten {
1923 fn exists(field_attrs: &[Attribute]) -> bool {
1925 for meta in field_attrs.iter().filter_map(|a| a.parse_meta().ok()) {
1926 let inner_meta = match meta {
1927 Meta::List(ref l)
1928 if l.path
1929 .segments
1930 .last()
1931 .map(|p| p.ident == "serde")
1932 .unwrap_or(false) =>
1933 {
1934 &l.nested
1935 }
1936 _ => continue,
1937 };
1938
1939 for meta in inner_meta {
1940 if let NestedMeta::Meta(Meta::Path(syn::Path { segments, .. })) = meta {
1941 if segments.iter().any(|p| p.ident == "flatten") {
1942 return true;
1943 }
1944 }
1945 }
1946 }
1947
1948 false
1949 }
1950}
1951
1952macro_rules! doc_comment {
1953 ($x:expr; $($tt:tt)*) => {
1954 #[doc = $x]
1955 $($tt)*
1956 };
1957}
1958
1959#[cfg(feature = "actix")]
1960impl super::Method {
1961 fn handler_uri(attr: TokenStream) -> TokenStream {
1962 let attr = parse_macro_input!(attr as syn::AttributeArgs);
1963 attr.first().into_token_stream().into()
1964 }
1965 fn handler_name(item: TokenStream) -> syn::Result<syn::Ident> {
1966 let handler: ItemFn = syn::parse(item)?;
1967 Ok(handler.sig.ident)
1968 }
1969 pub(crate) fn generate(
1970 &self,
1971 attr: TokenStream,
1972 item: TokenStream,
1973 ) -> syn::Result<proc_macro2::TokenStream> {
1974 let uri: proc_macro2::TokenStream = Self::handler_uri(attr).into();
1975 let handler_name = Self::handler_name(item.clone())?;
1976 let handler_fn: proc_macro2::TokenStream = item.into();
1977 let method: proc_macro2::TokenStream = self.method().parse()?;
1978 let variant: proc_macro2::TokenStream = self.variant().parse()?;
1979 let handler_name_str = handler_name.to_string();
1980
1981 let uri = uri.to_string().replace('\"', ""); let uri_fmt = if !uri.starts_with('/') {
1984 format!("/{}", uri)
1985 } else {
1986 uri
1987 };
1988
1989 Ok(quote! {
1990 #[allow(non_camel_case_types, missing_docs)]
1991 pub struct #handler_name;
1992
1993 impl #handler_name {
1994 fn resource() -> paperclip::actix::web::Resource {
1995 #handler_fn
1996 paperclip::actix::web::Resource::new(#uri_fmt)
1997 .name(#handler_name_str)
1998 .guard(actix_web::guard::#variant())
1999 .route(paperclip::actix::web::#method().to(#handler_name))
2000 }
2001 }
2002
2003 impl actix_web::dev::HttpServiceFactory for #handler_name {
2004 fn register(self, config: &mut actix_web::dev::AppService) {
2005 Self::resource().register(config);
2006 }
2007 }
2008
2009 impl paperclip::actix::Mountable for #handler_name {
2010 fn path(&self) -> &str {
2011 #uri_fmt
2012 }
2013
2014 fn operations(
2015 &mut self,
2016 ) -> std::collections::BTreeMap<
2017 paperclip::v2::models::HttpMethod,
2018 paperclip::v2::models::DefaultOperationRaw,
2019 > {
2020 Self::resource().operations()
2021 }
2022
2023 fn definitions(
2024 &mut self,
2025 ) -> std::collections::BTreeMap<
2026 String,
2027 paperclip::v2::models::DefaultSchemaRaw,
2028 > {
2029 Self::resource().definitions()
2030 }
2031
2032 fn security_definitions(
2033 &mut self,
2034 ) -> std::collections::BTreeMap<String, paperclip::v2::models::SecurityScheme>
2035 {
2036 Self::resource().security_definitions()
2037 }
2038 }
2039 })
2040 }
2041}
2042
2043macro_rules! rest_methods {
2044 (
2045 $($variant:ident, $method:ident, )+
2046 ) => {
2047 #[derive(Debug, PartialEq, Eq, Hash)]
2049 pub(crate) enum Method {
2050 $(
2051 $variant,
2052 )+
2053 }
2054
2055 impl Method {
2056 fn method(&self) -> &'static str {
2057 match self {
2058 $(Self::$variant => stringify!($method),)+
2059 }
2060 }
2061 fn variant(&self) -> &'static str {
2062 match self {
2063 $(Self::$variant => stringify!($variant),)+
2064 }
2065 }
2066 }
2067
2068 $(doc_comment! {
2069 concat!("
2070Creates route handler with `paperclip::actix::web::Resource", "`.
2071In order to control the output type and status codes the return value/response must implement the
2072trait actix_web::Responder.
2073
2074# Syntax
2075```text
2076#[", stringify!($method), r#"("path"[, attributes])]
2077```
2078
2079# Attributes
2080- `"path"` - Raw literal string with path for which to register handler.
2081
2082# Example
2083
2084/// use paperclip::actix::web::Json;
2085/// use paperclip_macros::"#, stringify!($method), ";
2086/// #[", stringify!($method), r#"("/")]
2087/// async fn example() {
2088/// }
2089"#);
2090 #[cfg(feature = "actix")]
2091 #[proc_macro_attribute]
2092 pub fn $method(attr: TokenStream, item: TokenStream) -> TokenStream {
2093 match Method::$variant.generate(attr, item) {
2094 Ok(v) => v.into(),
2095 Err(e) => e.to_compile_error().into(),
2096 }
2097 }
2098 })+
2099 };
2100}