omni_schema_derive/
lib.rs

1
2//! # omni-schema-derive
3//!
4//! Derive macros for the omni-schema library.
5//!
6//! This crate provides the `#[derive(Schema)]` macro that generates schema
7//! definitions from Rust structs and enums.
8//!
9//! ## Usage
10//!
11//! ```rust,ignore
12//! use omni_schema::Schema;
13//!
14//! #[derive(Schema)]
15//! #[schema(description = "A user in the system")]
16//! pub struct User {
17//!     #[schema(description = "Unique identifier")]
18//!     pub id: u64,
19//!
20//!     #[schema(min_length = 1, max_length = 100)]
21//!     pub name: String,
22//!
23//!     #[schema(format = "email")]
24//!     pub email: String,
25//! }
26//! ```
27//!
28//! ## Supported Attributes
29//!
30//! ### Type-level attributes
31//!
32//! - `description` - Documentation for the type
33//! - `rename` - Custom name for the schema
34//! - `rename_all` - Rename all fields (camelCase, snake_case, PascalCase, etc.)
35//! - `deprecated` - Mark the type as deprecated
36//! - `tag` - Tag field name for internally tagged enums
37//! - `content` - Content field name for adjacently tagged enums
38//! - `untagged` - Use untagged enum representation
39//!
40//! ### Field-level attributes
41//!
42//! - `description` - Documentation for the field
43//! - `rename` - Custom name for the field
44//! - `min_length` / `max_length` - String length constraints
45//! - `minimum` / `maximum` - Numeric range constraints
46//! - `pattern` - Regex pattern for string validation
47//! - `format` - Format hint (email, uri, uuid, etc.)
48//! - `default` - Default value expression
49//! - `deprecated` - Mark the field as deprecated
50//! - `skip` - Skip this field in schema generation
51//! - `flatten` - Flatten nested struct fields
52//! - `nullable` - Mark field as nullable
53
54use proc_macro::TokenStream;
55use proc_macro2::TokenStream as TokenStream2;
56use quote::quote;
57use syn::{
58    parse_macro_input, Attribute, Data, DeriveInput, Expr, Field, Fields, GenericArgument, Ident,
59    Lit, Meta, PathArguments, Type,
60};
61
62#[proc_macro_derive(Schema, attributes(schema))]
63pub fn derive_schema(input: TokenStream) -> TokenStream {
64    let input = parse_macro_input!(input as DeriveInput);
65
66    match derive_schema_impl(&input) {
67        Ok(tokens) => tokens.into(),
68        Err(err) => err.to_compile_error().into(),
69    }
70}
71
72fn derive_schema_impl(input: &DeriveInput) -> syn::Result<TokenStream2> {
73    let name = &input.ident;
74    let name_str = name.to_string();
75
76    let type_attrs = parse_type_attributes(&input.attrs)?;
77    let schema_name = type_attrs.rename.as_deref().unwrap_or(&name_str);
78
79    let schema_def = match &input.data {
80        Data::Struct(data) => generate_struct_schema(name, &data.fields, &type_attrs)?,
81        Data::Enum(data) => generate_enum_schema(name, data, &type_attrs)?,
82        Data::Union(_) => {
83            return Err(syn::Error::new_spanned(
84                input,
85                "Schema cannot be derived for unions",
86            ))
87        }
88    };
89
90    let expanded = quote! {
91        impl ::omni_schema_core::Schema for #name {
92            fn schema_definition() -> ::omni_schema_core::SchemaDefinition {
93                #schema_def
94            }
95
96            fn schema_name() -> &'static str {
97                #schema_name
98            }
99        }
100    };
101
102    Ok(expanded)
103}
104
105#[derive(Default)]
106struct TypeAttributes {
107    description: Option<String>,
108    rename: Option<String>,
109    rename_all: Option<String>,
110    deprecated: bool,
111    tag: Option<String>,
112    content: Option<String>,
113    untagged: bool,
114}
115
116#[derive(Default)]
117struct FieldAttributes {
118    description: Option<String>,
119    rename: Option<String>,
120    min_length: Option<usize>,
121    max_length: Option<usize>,
122    minimum: Option<f64>,
123    maximum: Option<f64>,
124    pattern: Option<String>,
125    format: Option<String>,
126    default: Option<Expr>,
127    deprecated: bool,
128    skip: bool,
129    flatten: bool,
130    nullable: Option<bool>,
131}
132
133fn parse_type_attributes(attrs: &[Attribute]) -> syn::Result<TypeAttributes> {
134    let mut result = TypeAttributes::default();
135
136    for attr in attrs {
137        if !attr.path().is_ident("schema") {
138            continue;
139        }
140
141        attr.parse_nested_meta(|meta| {
142            if meta.path.is_ident("description") {
143                let value: Lit = meta.value()?.parse()?;
144                if let Lit::Str(s) = value {
145                    result.description = Some(s.value());
146                }
147            } else if meta.path.is_ident("rename") {
148                let value: Lit = meta.value()?.parse()?;
149                if let Lit::Str(s) = value {
150                    result.rename = Some(s.value());
151                }
152            } else if meta.path.is_ident("rename_all") {
153                let value: Lit = meta.value()?.parse()?;
154                if let Lit::Str(s) = value {
155                    result.rename_all = Some(s.value());
156                }
157            } else if meta.path.is_ident("deprecated") {
158                result.deprecated = true;
159            } else if meta.path.is_ident("tag") {
160                let value: Lit = meta.value()?.parse()?;
161                if let Lit::Str(s) = value {
162                    result.tag = Some(s.value());
163                }
164            } else if meta.path.is_ident("content") {
165                let value: Lit = meta.value()?.parse()?;
166                if let Lit::Str(s) = value {
167                    result.content = Some(s.value());
168                }
169            } else if meta.path.is_ident("untagged") {
170                result.untagged = true;
171            }
172            Ok(())
173        })?;
174    }
175
176    for attr in attrs {
177        if attr.path().is_ident("doc") {
178            if let Meta::NameValue(meta) = &attr.meta {
179                if let Expr::Lit(expr_lit) = &meta.value {
180                    if let Lit::Str(s) = &expr_lit.lit {
181                        let doc = s.value().trim().to_string();
182                        if result.description.is_none() {
183                            result.description = Some(doc);
184                        } else if let Some(ref mut desc) = result.description {
185                            desc.push('\n');
186                            desc.push_str(&doc);
187                        }
188                    }
189                }
190            }
191        }
192    }
193
194    Ok(result)
195}
196
197fn parse_field_attributes(attrs: &[Attribute]) -> syn::Result<FieldAttributes> {
198    let mut result = FieldAttributes::default();
199
200    for attr in attrs {
201        if !attr.path().is_ident("schema") {
202            continue;
203        }
204
205        attr.parse_nested_meta(|meta| {
206            if meta.path.is_ident("description") {
207                let value: Lit = meta.value()?.parse()?;
208                if let Lit::Str(s) = value {
209                    result.description = Some(s.value());
210                }
211            } else if meta.path.is_ident("rename") {
212                let value: Lit = meta.value()?.parse()?;
213                if let Lit::Str(s) = value {
214                    result.rename = Some(s.value());
215                }
216            } else if meta.path.is_ident("min_length") {
217                let value: Lit = meta.value()?.parse()?;
218                if let Lit::Int(i) = value {
219                    result.min_length = Some(i.base10_parse()?);
220                }
221            } else if meta.path.is_ident("max_length") {
222                let value: Lit = meta.value()?.parse()?;
223                if let Lit::Int(i) = value {
224                    result.max_length = Some(i.base10_parse()?);
225                }
226            } else if meta.path.is_ident("minimum") {
227                let value: Lit = meta.value()?.parse()?;
228                match value {
229                    Lit::Int(i) => result.minimum = Some(i.base10_parse::<i64>()? as f64),
230                    Lit::Float(f) => result.minimum = Some(f.base10_parse()?),
231                    _ => {}
232                }
233            } else if meta.path.is_ident("maximum") {
234                let value: Lit = meta.value()?.parse()?;
235                match value {
236                    Lit::Int(i) => result.maximum = Some(i.base10_parse::<i64>()? as f64),
237                    Lit::Float(f) => result.maximum = Some(f.base10_parse()?),
238                    _ => {}
239                }
240            } else if meta.path.is_ident("pattern") {
241                let value: Lit = meta.value()?.parse()?;
242                if let Lit::Str(s) = value {
243                    result.pattern = Some(s.value());
244                }
245            } else if meta.path.is_ident("format") {
246                let value: Lit = meta.value()?.parse()?;
247                if let Lit::Str(s) = value {
248                    result.format = Some(s.value());
249                }
250            } else if meta.path.is_ident("default") {
251                let value: Expr = meta.value()?.parse()?;
252                result.default = Some(value);
253            } else if meta.path.is_ident("deprecated") {
254                result.deprecated = true;
255            } else if meta.path.is_ident("skip") {
256                result.skip = true;
257            } else if meta.path.is_ident("flatten") {
258                result.flatten = true;
259            } else if meta.path.is_ident("nullable") {
260                result.nullable = Some(true);
261            }
262            Ok(())
263        })?;
264    }
265
266    for attr in attrs {
267        if attr.path().is_ident("doc") {
268            if let Meta::NameValue(meta) = &attr.meta {
269                if let Expr::Lit(expr_lit) = &meta.value {
270                    if let Lit::Str(s) = &expr_lit.lit {
271                        let doc = s.value().trim().to_string();
272                        if result.description.is_none() {
273                            result.description = Some(doc);
274                        } else if let Some(ref mut desc) = result.description {
275                            desc.push('\n');
276                            desc.push_str(&doc);
277                        }
278                    }
279                }
280            }
281        }
282    }
283
284    Ok(result)
285}
286
287fn generate_struct_schema(
288    name: &Ident,
289    fields: &Fields,
290    type_attrs: &TypeAttributes,
291) -> syn::Result<TokenStream2> {
292    let name_str = type_attrs.rename.as_deref().unwrap_or(&name.to_string()).to_string();
293
294    match fields {
295        Fields::Named(named) => {
296            let field_defs = generate_named_fields(&named.named, type_attrs)?;
297
298            let description = match &type_attrs.description {
299                Some(desc) => quote! { .with_description(#desc) },
300                None => quote! {},
301            };
302
303            let deprecated = if type_attrs.deprecated {
304                quote! { .deprecated() }
305            } else {
306                quote! {}
307            };
308
309            Ok(quote! {
310                {
311                    let mut struct_def = ::omni_schema_core::types::StructDefinition::new();
312                    #field_defs
313                    ::omni_schema_core::SchemaDefinition::new(
314                        #name_str,
315                        ::omni_schema_core::types::SchemaType::Struct(struct_def),
316                    )
317                    #description
318                    #deprecated
319                }
320            })
321        }
322        Fields::Unnamed(unnamed) => {
323            let field_defs = generate_tuple_fields(&unnamed.unnamed)?;
324
325            let description = match &type_attrs.description {
326                Some(desc) => quote! { .with_description(#desc) },
327                None => quote! {},
328            };
329
330            let deprecated = if type_attrs.deprecated {
331                quote! { .deprecated() }
332            } else {
333                quote! {}
334            };
335
336            Ok(quote! {
337                {
338                    let mut struct_def = ::omni_schema_core::types::StructDefinition::tuple();
339                    #field_defs
340                    ::omni_schema_core::SchemaDefinition::new(
341                        #name_str,
342                        ::omni_schema_core::types::SchemaType::Struct(struct_def),
343                    )
344                    #description
345                    #deprecated
346                }
347            })
348        }
349        Fields::Unit => {
350            let description = match &type_attrs.description {
351                Some(desc) => quote! { .with_description(#desc) },
352                None => quote! {},
353            };
354
355            let deprecated = if type_attrs.deprecated {
356                quote! { .deprecated() }
357            } else {
358                quote! {}
359            };
360
361            Ok(quote! {
362                ::omni_schema_core::SchemaDefinition::new(
363                    #name_str,
364                    ::omni_schema_core::types::SchemaType::Unit,
365                )
366                #description
367                #deprecated
368            })
369        }
370    }
371}
372
373fn generate_named_fields<'a>(
374    fields: impl IntoIterator<Item = &'a Field>,
375    type_attrs: &TypeAttributes,
376) -> syn::Result<TokenStream2> {
377    let mut tokens = TokenStream2::new();
378
379    for field in fields {
380        let field_attrs = parse_field_attributes(&field.attrs)?;
381
382        if field_attrs.skip {
383            continue;
384        }
385
386        let field_name = field.ident.as_ref().unwrap();
387        let field_name_str = field_attrs
388            .rename
389            .as_deref()
390            .map(|s| s.to_string())
391            .unwrap_or_else(|| {
392                apply_rename_rule(&field_name.to_string(), type_attrs.rename_all.as_deref())
393            });
394
395        let original_name_str = field_name.to_string();
396        let ty = &field.ty;
397        let schema_type = generate_type_schema(ty);
398
399        let mut field_builder = quote! {
400            ::omni_schema_core::types::StructField::new(#schema_type, #original_name_str)
401        };
402
403        if let Some(ref desc) = field_attrs.description {
404            field_builder = quote! { #field_builder.with_description(#desc) };
405        }
406
407        if let Some(ref format) = field_attrs.format {
408            field_builder = quote! { #field_builder.with_format(#format) };
409        }
410
411        if let Some(min) = field_attrs.min_length {
412            field_builder = quote! { #field_builder.with_length(Some(#min), None) };
413        }
414
415        if let Some(max) = field_attrs.max_length {
416            if field_attrs.min_length.is_some() {
417                let min = field_attrs.min_length.unwrap();
418                field_builder = quote! {
419                    {
420                        let mut f = #field_builder;
421                        f.min_length = Some(#min);
422                        f.max_length = Some(#max);
423                        f
424                    }
425                };
426            } else {
427                field_builder = quote! { #field_builder.with_length(None, Some(#max)) };
428            }
429        }
430
431        if let Some(min) = field_attrs.minimum {
432            field_builder = quote! { #field_builder.with_range(Some(#min), None) };
433        }
434
435        if let Some(max) = field_attrs.maximum {
436            if field_attrs.minimum.is_some() {
437                let min = field_attrs.minimum.unwrap();
438                field_builder = quote! { #field_builder.with_range(Some(#min), Some(#max)) };
439            } else {
440                field_builder = quote! { #field_builder.with_range(None, Some(#max)) };
441            }
442        }
443
444        if let Some(ref pattern) = field_attrs.pattern {
445            field_builder = quote! { #field_builder.with_pattern(#pattern) };
446        }
447
448        if field_attrs.deprecated {
449            field_builder = quote! { #field_builder.deprecated() };
450        }
451
452        if field_attrs.flatten {
453            field_builder = quote! { #field_builder.flattened() };
454        }
455
456        if let Some(true) = field_attrs.nullable {
457            field_builder = quote! { #field_builder.nullable() };
458        }
459
460        if let Some(ref default_expr) = field_attrs.default {
461            field_builder = quote! {
462                #field_builder.with_default(::serde_json::json!(#default_expr))
463            };
464        }
465
466        tokens.extend(quote! {
467            struct_def = struct_def.with_field(#field_name_str, #field_builder);
468        });
469    }
470
471    Ok(tokens)
472}
473
474fn generate_tuple_fields<'a>(
475    fields: impl IntoIterator<Item = &'a Field>,
476) -> syn::Result<TokenStream2> {
477    let mut tokens = TokenStream2::new();
478
479    for (i, field) in fields.into_iter().enumerate() {
480        let field_attrs = parse_field_attributes(&field.attrs)?;
481        let field_name = format!("{}", i);
482        let ty = &field.ty;
483        let schema_type = generate_type_schema(ty);
484
485        let mut field_builder = quote! {
486            ::omni_schema_core::types::StructField::new(#schema_type, #field_name)
487        };
488
489        if let Some(ref desc) = field_attrs.description {
490            field_builder = quote! { #field_builder.with_description(#desc) };
491        }
492
493        tokens.extend(quote! {
494            struct_def = struct_def.with_field(#field_name, #field_builder);
495        });
496    }
497
498    Ok(tokens)
499}
500
501fn generate_enum_schema(
502    name: &Ident,
503    data: &syn::DataEnum,
504    type_attrs: &TypeAttributes,
505) -> syn::Result<TokenStream2> {
506    let name_str = type_attrs.rename.as_deref().unwrap_or(&name.to_string()).to_string();
507
508    let representation = if type_attrs.untagged {
509        quote! { ::omni_schema_core::types::EnumRepresentation::Untagged }
510    } else if let Some(ref tag) = type_attrs.tag {
511        if let Some(ref content) = type_attrs.content {
512            quote! { ::omni_schema_core::types::EnumRepresentation::Adjacent {
513                tag: #tag.to_string(),
514                content: #content.to_string()
515            }}
516        } else {
517            quote! { ::omni_schema_core::types::EnumRepresentation::Internal {
518                tag: #tag.to_string()
519            }}
520        }
521    } else {
522        quote! { ::omni_schema_core::types::EnumRepresentation::External }
523    };
524
525    let mut variant_tokens = TokenStream2::new();
526
527    for variant in &data.variants {
528        let variant_attrs = parse_field_attributes(&variant.attrs)?;
529        let variant_name = &variant.ident;
530        let variant_name_str = variant_attrs
531            .rename
532            .as_deref()
533            .map(|s| s.to_string())
534            .unwrap_or_else(|| {
535                apply_rename_rule(&variant_name.to_string(), type_attrs.rename_all.as_deref())
536            });
537
538        let variant_def = match &variant.fields {
539            Fields::Unit => {
540                quote! { ::omni_schema_core::types::EnumVariant::unit(#variant_name_str) }
541            }
542            Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
543                let ty = &fields.unnamed.first().unwrap().ty;
544                let schema_type = generate_type_schema(ty);
545                quote! { ::omni_schema_core::types::EnumVariant::newtype(#variant_name_str, #schema_type) }
546            }
547            Fields::Unnamed(fields) => {
548                let types: Vec<_> = fields.unnamed.iter().map(|f| {
549                    let ty = &f.ty;
550                    generate_type_schema(ty)
551                }).collect();
552                quote! { ::omni_schema_core::types::EnumVariant::tuple(#variant_name_str, vec![#(#types),*]) }
553            }
554            Fields::Named(fields) => {
555                    let field_tokens = generate_variant_fields(&fields.named, type_attrs)?;
556                    quote! {
557                        {
558                            let mut fields = ::omni_schema_core::__private::IndexMap::new();
559                            #field_tokens
560                            ::omni_schema_core::types::EnumVariant::struct_variant(#variant_name_str, fields)
561                        }
562                    }
563                }
564        };
565
566        let mut variant_builder = variant_def;
567
568        if let Some(ref desc) = variant_attrs.description {
569            variant_builder = quote! { #variant_builder.with_description(#desc) };
570        }
571
572        if variant_attrs.deprecated {
573            variant_builder = quote! { #variant_builder.deprecated() };
574        }
575
576        variant_tokens.extend(quote! {
577            enum_def = enum_def.with_variant(#variant_builder);
578        });
579    }
580
581    let description = match &type_attrs.description {
582        Some(desc) => quote! { .with_description(#desc) },
583        None => quote! {},
584    };
585
586    let deprecated = if type_attrs.deprecated {
587        quote! { .deprecated() }
588    } else {
589        quote! {}
590    };
591
592    Ok(quote! {
593        {
594            let mut enum_def = ::omni_schema_core::types::EnumDefinition::new(#representation);
595            #variant_tokens
596            ::omni_schema_core::SchemaDefinition::new(
597                #name_str,
598                ::omni_schema_core::types::SchemaType::Enum(enum_def),
599            )
600            #description
601            #deprecated
602        }
603    })
604}
605
606fn generate_variant_fields<'a>(
607    fields: impl IntoIterator<Item = &'a Field>,
608    type_attrs: &TypeAttributes,
609) -> syn::Result<TokenStream2> {
610    let mut tokens = TokenStream2::new();
611
612    for field in fields {
613        let field_attrs = parse_field_attributes(&field.attrs)?;
614
615        if field_attrs.skip {
616            continue;
617        }
618
619        let field_name = field.ident.as_ref().unwrap();
620        let field_name_str = field_attrs
621            .rename
622            .as_deref()
623            .map(|s| s.to_string())
624            .unwrap_or_else(|| {
625                apply_rename_rule(&field_name.to_string(), type_attrs.rename_all.as_deref())
626            });
627
628        let original_name_str = field_name.to_string();
629        let ty = &field.ty;
630        let schema_type = generate_type_schema(ty);
631
632        let mut field_builder = quote! {
633            ::omni_schema_core::types::StructField::new(#schema_type, #original_name_str)
634        };
635
636        if let Some(ref desc) = field_attrs.description {
637            field_builder = quote! { #field_builder.with_description(#desc) };
638        }
639
640        if let Some(ref format) = field_attrs.format {
641            field_builder = quote! { #field_builder.with_format(#format) };
642        }
643
644        if field_attrs.deprecated {
645            field_builder = quote! { #field_builder.deprecated() };
646        }
647
648        tokens.extend(quote! {
649            fields.insert(#field_name_str.to_string(), #field_builder);
650        });
651    }
652
653    Ok(tokens)
654}
655
656fn generate_type_schema(ty: &Type) -> TokenStream2 {
657    match ty {
658        Type::Path(type_path) => {
659            let path = &type_path.path;
660            let segment = path.segments.last().unwrap();
661            let type_name = segment.ident.to_string();
662
663            match type_name.as_str() {
664                "bool" => quote! { ::omni_schema_core::types::SchemaType::Primitive(::omni_schema_core::types::PrimitiveType::Bool) },
665                "i8" => quote! { ::omni_schema_core::types::SchemaType::Primitive(::omni_schema_core::types::PrimitiveType::I8) },
666                "i16" => quote! { ::omni_schema_core::types::SchemaType::Primitive(::omni_schema_core::types::PrimitiveType::I16) },
667                "i32" => quote! { ::omni_schema_core::types::SchemaType::Primitive(::omni_schema_core::types::PrimitiveType::I32) },
668                "i64" => quote! { ::omni_schema_core::types::SchemaType::Primitive(::omni_schema_core::types::PrimitiveType::I64) },
669                "i128" => quote! { ::omni_schema_core::types::SchemaType::Primitive(::omni_schema_core::types::PrimitiveType::I128) },
670                "isize" => quote! { ::omni_schema_core::types::SchemaType::Primitive(::omni_schema_core::types::PrimitiveType::Isize) },
671                "u8" => quote! { ::omni_schema_core::types::SchemaType::Primitive(::omni_schema_core::types::PrimitiveType::U8) },
672                "u16" => quote! { ::omni_schema_core::types::SchemaType::Primitive(::omni_schema_core::types::PrimitiveType::U16) },
673                "u32" => quote! { ::omni_schema_core::types::SchemaType::Primitive(::omni_schema_core::types::PrimitiveType::U32) },
674                "u64" => quote! { ::omni_schema_core::types::SchemaType::Primitive(::omni_schema_core::types::PrimitiveType::U64) },
675                "u128" => quote! { ::omni_schema_core::types::SchemaType::Primitive(::omni_schema_core::types::PrimitiveType::U128) },
676                "usize" => quote! { ::omni_schema_core::types::SchemaType::Primitive(::omni_schema_core::types::PrimitiveType::Usize) },
677                "f32" => quote! { ::omni_schema_core::types::SchemaType::Primitive(::omni_schema_core::types::PrimitiveType::F32) },
678                "f64" => quote! { ::omni_schema_core::types::SchemaType::Primitive(::omni_schema_core::types::PrimitiveType::F64) },
679                "char" => quote! { ::omni_schema_core::types::SchemaType::Primitive(::omni_schema_core::types::PrimitiveType::Char) },
680                "String" => quote! { ::omni_schema_core::types::SchemaType::Primitive(::omni_schema_core::types::PrimitiveType::String) },
681
682                "Option" => {
683                    if let PathArguments::AngleBracketed(args) = &segment.arguments {
684                        if let Some(GenericArgument::Type(inner_ty)) = args.args.first() {
685                            let inner = generate_type_schema(inner_ty);
686                            return quote! { ::omni_schema_core::types::SchemaType::Option(Box::new(#inner)) };
687                        }
688                    }
689                    quote! { ::omni_schema_core::types::SchemaType::Any }
690                }
691                "Vec" => {
692                    if let PathArguments::AngleBracketed(args) = &segment.arguments {
693                        if let Some(GenericArgument::Type(inner_ty)) = args.args.first() {
694                            let inner = generate_type_schema(inner_ty);
695                            return quote! { ::omni_schema_core::types::SchemaType::Array(Box::new(#inner)) };
696                        }
697                    }
698                    quote! { ::omni_schema_core::types::SchemaType::Any }
699                }
700                "HashSet" | "BTreeSet" => {
701                    if let PathArguments::AngleBracketed(args) = &segment.arguments {
702                        if let Some(GenericArgument::Type(inner_ty)) = args.args.first() {
703                            let inner = generate_type_schema(inner_ty);
704                            return quote! { ::omni_schema_core::types::SchemaType::Set(Box::new(#inner)) };
705                        }
706                    }
707                    quote! { ::omni_schema_core::types::SchemaType::Any }
708                }
709                "HashMap" | "BTreeMap" => {
710                    if let PathArguments::AngleBracketed(args) = &segment.arguments {
711                        let mut args_iter = args.args.iter();
712                        args_iter.next(); // Skip key type
713                        if let Some(GenericArgument::Type(value_ty)) = args_iter.next() {
714                            let value = generate_type_schema(value_ty);
715                            return quote! { ::omni_schema_core::types::SchemaType::Map(Box::new(#value)) };
716                        }
717                    }
718                    quote! { ::omni_schema_core::types::SchemaType::Any }
719                }
720                "Box" => {
721                    if let PathArguments::AngleBracketed(args) = &segment.arguments {
722                        if let Some(GenericArgument::Type(inner_ty)) = args.args.first() {
723                            return generate_type_schema(inner_ty);
724                        }
725                    }
726                    quote! { ::omni_schema_core::types::SchemaType::Any }
727                }
728
729                "Value" => quote! { ::omni_schema_core::types::SchemaType::Any },
730
731                _ => {
732                    let type_name_str = type_name;
733                    quote! { ::omni_schema_core::types::SchemaType::Reference(#type_name_str.to_string()) }
734                }
735            }
736        }
737        Type::Reference(type_ref) => {
738            if let Type::Path(path) = &*type_ref.elem {
739                if let Some(segment) = path.path.segments.last() {
740                    if segment.ident == "str" {
741                        return quote! { ::omni_schema_core::types::SchemaType::Primitive(::omni_schema_core::types::PrimitiveType::String) };
742                    }
743                }
744            }
745            generate_type_schema(&type_ref.elem)
746        }
747        Type::Tuple(tuple) => {
748            if tuple.elems.is_empty() {
749                quote! { ::omni_schema_core::types::SchemaType::Unit }
750            } else {
751                let elem_types: Vec<_> = tuple.elems.iter().map(|ty| {
752                    let inner = generate_type_schema(ty);
753                    quote! { Box::new(#inner) }
754                }).collect();
755                quote! { ::omni_schema_core::types::SchemaType::Tuple(vec![#(#elem_types),*]) }
756            }
757        }
758        Type::Array(array) => {
759            let inner = generate_type_schema(&array.elem);
760            quote! { ::omni_schema_core::types::SchemaType::Array(Box::new(#inner)) }
761        }
762        Type::Slice(slice) => {
763            let inner = generate_type_schema(&slice.elem);
764            quote! { ::omni_schema_core::types::SchemaType::Array(Box::new(#inner)) }
765        }
766        _ => {
767            quote! { ::omni_schema_core::types::SchemaType::Any }
768        }
769    }
770}
771
772fn apply_rename_rule(name: &str, rule: Option<&str>) -> String {
773    match rule {
774        Some("camelCase") => to_camel_case(name),
775        Some("snake_case") => to_snake_case(name),
776        Some("PascalCase") => to_pascal_case(name),
777        Some("SCREAMING_SNAKE_CASE") => to_snake_case(name).to_uppercase(),
778        Some("kebab-case") => to_snake_case(name).replace('_', "-"),
779        Some("SCREAMING-KEBAB-CASE") => to_snake_case(name).replace('_', "-").to_uppercase(),
780        Some("lowercase") => name.to_lowercase(),
781        Some("UPPERCASE") => name.to_uppercase(),
782        _ => name.to_string(),
783    }
784}
785
786fn to_camel_case(s: &str) -> String {
787    let pascal = to_pascal_case(s);
788    let mut chars = pascal.chars();
789    match chars.next() {
790        None => String::new(),
791        Some(first) => first.to_lowercase().chain(chars).collect(),
792    }
793}
794
795fn to_pascal_case(s: &str) -> String {
796    let mut result = String::with_capacity(s.len());
797    let mut capitalize_next = true;
798
799    for c in s.chars() {
800        if c == '_' || c == '-' || c == ' ' {
801            capitalize_next = true;
802        } else if capitalize_next {
803            result.extend(c.to_uppercase());
804            capitalize_next = false;
805        } else {
806            result.push(c);
807        }
808    }
809
810    result
811}
812
813fn to_snake_case(s: &str) -> String {
814    let mut result = String::with_capacity(s.len() + 4);
815    let mut prev_was_uppercase = false;
816    let mut prev_was_separator = true;
817
818    for c in s.chars() {
819        if c == '-' || c == ' ' {
820            result.push('_');
821            prev_was_separator = true;
822            prev_was_uppercase = false;
823        } else if c.is_uppercase() {
824            if !prev_was_separator && !prev_was_uppercase {
825                result.push('_');
826            }
827            result.extend(c.to_lowercase());
828            prev_was_uppercase = true;
829            prev_was_separator = false;
830        } else {
831            result.push(c);
832            prev_was_uppercase = false;
833            prev_was_separator = c == '_';
834        }
835    }
836
837    result
838}