elif_openapi_derive/
lib.rs1use proc_macro::TokenStream;
8use quote::quote;
9use syn::{parse_macro_input, DeriveInput, Data, Fields};
10
11#[proc_macro_derive(OpenApiSchema)]
13pub fn derive_openapi_schema(input: TokenStream) -> TokenStream {
14 let input = parse_macro_input!(input as DeriveInput);
15 generate_openapi_schema_impl(&input).unwrap_or_else(|err| err.to_compile_error().into())
16}
17
18fn generate_openapi_schema_impl(input: &DeriveInput) -> Result<TokenStream, syn::Error> {
20 let name = &input.ident;
21 let name_str = name.to_string();
22
23 let schema_impl = match &input.data {
24 Data::Struct(data_struct) => generate_struct_schema_impl(&name_str, &data_struct.fields)?,
25 Data::Enum(data_enum) => generate_enum_schema_impl(&name_str, data_enum)?,
26 Data::Union(_) => {
27 return Err(syn::Error::new_spanned(
28 input,
29 "OpenApiSchema cannot be derived for union types"
30 ));
31 }
32 };
33
34 let expanded = quote! {
35 impl ::elif_openapi::OpenApiSchema for #name {
36 fn openapi_schema() -> ::elif_openapi::specification::Schema {
37 #schema_impl
38 }
39
40 fn schema_name() -> String {
41 #name_str.to_string()
42 }
43 }
44 };
45
46 Ok(expanded.into())
47}
48
49fn generate_struct_schema_impl(type_name: &str, fields: &Fields) -> Result<proc_macro2::TokenStream, syn::Error> {
51 match fields {
52 Fields::Named(named_fields) => {
53 let mut properties = Vec::new();
55 let mut required = Vec::new();
56
57 for field in &named_fields.named {
58 let field_name = field.ident.as_ref().unwrap().to_string();
59 let field_type = &field.ty;
60
61 let is_optional = is_option_type(field_type);
63
64 if !is_optional {
65 required.push(field_name.clone());
66 }
67
68 let property_schema = quote! {
70 <#field_type as ::elif_openapi::OpenApiSchema>::openapi_schema()
71 };
72
73 properties.push(quote! {
74 properties.insert(#field_name.to_string(), #property_schema);
75 });
76 }
77
78 let required_fields = if required.is_empty() {
79 quote! { Vec::new() }
80 } else {
81 quote! { vec![#(#required.to_string()),*] }
82 };
83
84 Ok(quote! {
85 {
86 let mut properties = std::collections::HashMap::new();
87 #(#properties)*
88
89 ::elif_openapi::specification::Schema {
90 schema_type: Some("object".to_string()),
91 title: Some(#type_name.to_string()),
92 properties: properties,
93 required: #required_fields,
94 ..Default::default()
95 }
96 }
97 })
98 }
99 Fields::Unnamed(unnamed_fields) => {
100 if unnamed_fields.unnamed.len() == 1 {
102 let field_type = &unnamed_fields.unnamed.first().unwrap().ty;
104 Ok(quote! {
105 <#field_type as ::elif_openapi::OpenApiSchema>::openapi_schema()
106 })
107 } else {
108 let type_descriptions: Vec<String> = unnamed_fields.unnamed.iter()
111 .map(|field| quote::quote!(#field.ty).to_string())
112 .collect();
113
114 let field_count = unnamed_fields.unnamed.len();
115
116 let description = format!(
117 "A tuple with {} fields in fixed order: ({}). Note: OpenAPI 3.0 cannot precisely represent tuple types - this is a generic array representation.",
118 field_count,
119 type_descriptions.join(", ")
120 );
121
122 Ok(quote! {
123 ::elif_openapi::specification::Schema {
124 schema_type: Some("array".to_string()),
125 title: Some(#type_name.to_string()),
126 description: Some(#description.to_string()),
127 items: Some(Box::new(::elif_openapi::specification::Schema {
131 description: Some("Tuple element (type varies by position)".to_string()),
132 ..Default::default()
133 })),
134 ..Default::default()
135 }
136 })
137 }
138 }
139 Fields::Unit => {
140 Ok(quote! {
142 ::elif_openapi::specification::Schema {
143 schema_type: Some("null".to_string()),
144 title: Some(#type_name.to_string()),
145 ..Default::default()
146 }
147 })
148 }
149 }
150}
151
152fn generate_enum_schema_impl(type_name: &str, data_enum: &syn::DataEnum) -> Result<proc_macro2::TokenStream, syn::Error> {
154 let variants: Vec<String> = data_enum.variants.iter()
156 .map(|variant| variant.ident.to_string())
157 .collect();
158
159 Ok(quote! {
160 ::elif_openapi::specification::Schema {
161 schema_type: Some("string".to_string()),
162 title: Some(#type_name.to_string()),
163 enum_values: vec![#(serde_json::Value::String(#variants.to_string())),*],
164 ..Default::default()
165 }
166 })
167}
168
169fn is_option_type(ty: &syn::Type) -> bool {
171 if let syn::Type::Path(type_path) = ty {
172 if let Some(segment) = type_path.path.segments.last() {
173 return segment.ident == "Option";
174 }
175 }
176 false
177}