1#![allow(non_snake_case)]
2
3use proc_macro::TokenStream;
4use quote::quote;
5use serde_derive_internals::{
6 ast::{Container, Data, Style},
7 Ctxt, Derive,
8};
9use syn::{PathArguments, GenericArgument, TypePath, Type, ReturnType, FnArg, parse_macro_input, DeriveInput};
10use util::{derive_oaschema_enum, derive_oaschema_struct};
11use crate::attr::{get_docstring, OperationAttributes};
12use crate::util::derive_oaschema_newtype;
13
14mod util;
15mod attr;
16
17#[proc_macro_derive(OaSchema, attributes(oasgen))]
18pub fn derive_oaschema(item: TokenStream) -> TokenStream {
19 let ast = parse_macro_input!(item as DeriveInput);
20
21 let cont = {
22 let ctxt = Ctxt::new();
23 let cont = Container::from_ast(&ctxt, &ast, Derive::Deserialize);
24 ctxt.check().unwrap();
25 cont.unwrap()
26 };
27
28 let id = &cont.ident;
29 let docstring = get_docstring(&ast.attrs).expect("Failed to parse docstring");
30 match &cont.data {
31 Data::Struct(Style::Struct, fields) => {
32 derive_oaschema_struct(id, fields, docstring)
33 }
34 Data::Struct(Style::Newtype, fields) => {
35 derive_oaschema_newtype(id, fields.first().unwrap())
36 }
37 Data::Enum(variants) => {
38 derive_oaschema_enum(id, variants, &cont.attrs.tag(), docstring)
39 }
40 Data::Struct(Style::Tuple | Style::Unit, _) => {
41 panic!("#[derive(OaSchema)] can not be used on tuple structs")
42 }
43 }
44}
45
46
47#[proc_macro_attribute]
48pub fn oasgen(attr: TokenStream, input: TokenStream) -> TokenStream {
49 let ast = parse_macro_input!(input as syn::ItemFn);
50 let mut attr = syn::parse::<OperationAttributes>(attr).expect("Failed to parse operation attributes");
51 attr.merge_attributes(&ast.attrs);
52 let args = ast.sig.inputs.iter().map(|arg| {
53 match arg {
54 FnArg::Receiver(_) => panic!("Receiver arguments are not supported"),
55 FnArg::Typed(pat) => turbofish(pat.ty.as_ref().clone()),
56 }
57 }).collect::<Vec<_>>();
58 let ret = match &ast.sig.output {
59 ReturnType::Default => None,
60 ReturnType::Type(_, ty) => Some(turbofish(ty.as_ref().clone())),
61 };
62 let body = args.last().map(|t| {
63 quote! {
64 let body = <#t as ::oasgen::OaParameter>::body_schema();
65 if body.is_some() {
66 op.add_request_body_json(body);
67 }
68 }
69 }).unwrap_or_default();
70 let description = attr.description.as_ref().map(|s| s.value()).map(|c| {
71 quote! {
72 op.description = Some(#c.to_string());
73 }
74 }).unwrap_or_default();
75 let ret = ret.map(|t| {
76 quote! {
77 let body = <#t as ::oasgen::OaParameter>::body_schema();
78 if body.is_some() {
79 op.add_response_success_json(body);
80 }
81 }
82 }).unwrap_or_default();
83 let tags = attr.tags.iter().flatten().map(|s| {
84 quote! {
85 op.tags.push(#s.to_string());
86 }
87 }).collect::<Vec<_>>();
88 let summary = attr.summary.as_ref().map(|s| s.value()).map(|c| {
89 quote! {
90 op.summary = Some(#c.to_string());
91 }
92 }).unwrap_or_default();
93 let name = ast.sig.ident.to_string();
94 let deprecated = attr.deprecated;
95 let operation_id = if let Some(id) = attr.operation_id {
96 let id = id.value();
97 quote! {
98 Some(#id.to_string())
99 }
100 } else {
101 quote! {
102 ::oasgen::__private::fn_path_to_op_id(concat!(module_path!(), "::", #name))
103 }
104 };
105 let submit = quote! {
106 ::oasgen::register_operation!(concat!(module_path!(), "::", #name), || {
107 let parameters: Vec<Vec<::oasgen::RefOr<::oasgen::Parameter>>> = vec![
108 #( <#args as ::oasgen::OaParameter>::parameters(), )*
109 ];
110 let parameters = parameters
111 .into_iter()
112 .flatten()
113 .collect::<Vec<::oasgen::RefOr<::oasgen::Parameter>>>();
114 let mut op = ::oasgen::Operation::default();
115 op.operation_id = #operation_id;
116 op.parameters = parameters;
117 op.deprecated = #deprecated;
118 #body
119 #ret
120 #description
121 #summary
122 #(#tags)*
123 op
124 });
125 };
126 quote! {
127 #ast
128 #submit
129 }.into()
130}
131
132fn turbofish(mut ty: Type) -> Type {
135 fn inner(ty: &mut Type) {
136 match ty {
137 Type::Path(TypePath { path, .. }) => {
138 let Some(last) = path.segments.last_mut() else {
139 return;
140 };
141 match &mut last.arguments {
142 PathArguments::AngleBracketed(args) => {
143 args.colon2_token = Some(Default::default());
144 for arg in args.args.iter_mut() {
145 match arg {
146 GenericArgument::Type(ty) => {
147 inner(ty);
148 }
149 _ => {}
150 }
151 }
152 }
153 _ => {}
154 }
155 }
156 _ => {}
157 }
158 }
159 inner(&mut ty);
160 ty
161}
162
163#[cfg(test)]
164mod tests {
165 use quote::ToTokens;
166 use super::*;
167
168 #[test]
169 fn test_pathed_ty() {
170 let ty = syn::parse_str::<Type>("axum::Json<SendCode>").unwrap();
171 let ty = turbofish(ty);
172 assert_eq!(ty.to_token_stream().to_string(), "axum :: Json :: < SendCode >");
173 }
174}