oasgen_macro/
lib.rs

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
132/// insert the turbofish :: into a syn::Type
133/// example: axum::Json<User> becomes axum::Json::<User>
134fn 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}