elif_openapi_derive/
lib.rs

1/*!
2Procedural macros for OpenAPI schema generation.
3
4This crate provides derive macros for automatically implementing OpenAPI schema traits.
5*/
6
7use proc_macro::TokenStream;
8use quote::quote;
9use syn::{parse_macro_input, DeriveInput, Data, Fields};
10
11/// Derive macro to automatically implement OpenApiSchema for structs and enums
12#[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
18/// Generate implementation for OpenApiSchema trait
19fn 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
49/// Generate schema implementation for struct types
50fn generate_struct_schema_impl(type_name: &str, fields: &Fields) -> Result<proc_macro2::TokenStream, syn::Error> {
51    match fields {
52        Fields::Named(named_fields) => {
53            // Generate object schema with properties
54            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                // Check if field is optional (Option<T>)
62                let is_optional = is_option_type(field_type);
63                
64                if !is_optional {
65                    required.push(field_name.clone());
66                }
67
68                // Generate property schema
69                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            // Generate tuple schema
101            if unnamed_fields.unnamed.len() == 1 {
102                // Single field tuple - use the inner type's schema
103                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                // Multiple field tuple - use array with descriptive text
109                // OpenAPI 3.0 does not have good tuple support (OpenAPI 3.1 introduced prefixItems)
110                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                        // For OpenAPI 3.0, we use a generic array representation
128                        // OpenAPI 3.1 would use prefixItems for proper tuple support
129                        // Note: minItems/maxItems constraints are not available in this Schema implementation
130                        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            // Unit struct - use null schema
141            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
152/// Generate schema implementation for enum types
153fn generate_enum_schema_impl(type_name: &str, data_enum: &syn::DataEnum) -> Result<proc_macro2::TokenStream, syn::Error> {
154    // For now, generate simple string enum schema
155    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
169/// Helper function to check if a type is Option<T>
170fn 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}