Skip to main content

fm_rs_derive/
lib.rs

1//! Derive macro for the `Generable` trait.
2//!
3//! This crate provides a proc-macro derive for generating JSON Schema
4//! from Rust types for use with structured generation in fm-rs.
5
6//! Proc-macro derive for fm-rs Generable schema generation.
7
8#![allow(clippy::pedantic)]
9
10use proc_macro::TokenStream;
11use quote::{ToTokens, quote};
12use syn::{
13    Attribute, Data, DataEnum, DataStruct, DeriveInput, Fields, Lit, LitBool, LitStr, Meta, Path,
14    Type, parse_macro_input, spanned::Spanned,
15};
16
17/// Derives the `Generable` trait for a struct or enum.
18///
19/// This generates a JSON Schema that describes the type's structure,
20/// which can be used for structured generation with FoundationModels.
21///
22/// # Attributes
23///
24/// ## Container attributes (`#[generable(...)]` or `#[serde(...)]`)
25/// - `rename_all = "..."` - Rename all fields/variants (camelCase, snake_case, etc.)
26/// - `description = "..."` - Add a description to the schema
27///
28/// ## Field/variant attributes (`#[generable(...)]` or `#[serde(...)]`)
29/// - `rename = "..."` - Rename this field/variant
30/// - `skip` - Skip this field/variant in the schema
31/// - `description = "..."` - Add a description
32/// - `minimum = N` / `maximum = N` - Numeric bounds
33/// - `min_length = N` / `max_length = N` - String length bounds
34/// - `pattern = "..."` - Regex pattern for strings
35/// - `min_items = N` / `max_items = N` - Array length bounds
36///
37/// # Example
38///
39/// ```ignore
40/// use fm_rs::Generable;
41///
42/// #[derive(Generable)]
43/// #[generable(rename_all = "camelCase")]
44/// struct Person {
45///     #[generable(description = "The person's full name")]
46///     full_name: String,
47///     #[generable(minimum = 0, maximum = 150)]
48///     age: u32,
49/// }
50/// ```
51/// Derives `Generable` for structs and unit enums.
52#[proc_macro_derive(Generable, attributes(generable, serde))]
53pub fn derive_generable(input: TokenStream) -> TokenStream {
54    let input = parse_macro_input!(input as DeriveInput);
55    match derive_generable_impl(&input) {
56        Ok(tokens) => tokens.into(),
57        Err(err) => err.to_compile_error().into(),
58    }
59}
60
61struct ContainerAttrs {
62    crate_path: Path,
63    rename_all: Option<String>,
64    description: Option<String>,
65    example: Option<Lit>,
66}
67
68impl Default for ContainerAttrs {
69    fn default() -> Self {
70        let crate_path = match syn::parse_str::<Path>("::fm_rs") {
71            Ok(path) => path,
72            Err(_) => Path::from(syn::Ident::new("fm_rs", proc_macro2::Span::call_site())),
73        };
74        Self {
75            crate_path,
76            rename_all: None,
77            description: None,
78            example: None,
79        }
80    }
81}
82
83#[derive(Default)]
84struct FieldAttrs {
85    rename: Option<String>,
86    skip: bool,
87    default: bool,
88    description: Option<String>,
89    example: Option<Lit>,
90    minimum: Option<Lit>,
91    maximum: Option<Lit>,
92    min_length: Option<Lit>,
93    max_length: Option<Lit>,
94    pattern: Option<String>,
95    min_items: Option<Lit>,
96    max_items: Option<Lit>,
97    nullable: bool,
98}
99
100struct SchemaInfo {
101    expr: proc_macro2::TokenStream,
102    optional: bool,
103}
104
105fn derive_generable_impl(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
106    let container_attrs = parse_container_attrs(&input.attrs)?;
107    let crate_path = &container_attrs.crate_path;
108    let ident = &input.ident;
109    let schema_body = match &input.data {
110        Data::Struct(data) => schema_for_struct(data, &container_attrs)?,
111        Data::Enum(data) => schema_for_enum(data, &container_attrs)?,
112        Data::Union(_) => {
113            return Err(syn::Error::new(
114                input.span(),
115                "Generable does not support unions",
116            ));
117        }
118    };
119
120    let tokens = quote! {
121        impl #crate_path::Generable for #ident {
122            fn schema() -> #crate_path::__serde_json::Value {
123                #schema_body
124            }
125        }
126    };
127
128    Ok(tokens)
129}
130
131fn parse_container_attrs(attrs: &[Attribute]) -> syn::Result<ContainerAttrs> {
132    let mut out = ContainerAttrs::default();
133
134    for attr in attrs {
135        if attr.path().is_ident("generable") {
136            attr.parse_nested_meta(|meta| {
137                if meta.path.is_ident("crate") {
138                    let value = meta.value()?.parse::<LitStr>()?;
139                    out.crate_path = syn::parse_str(&value.value())
140                        .map_err(|_| syn::Error::new(value.span(), "invalid crate path"))?;
141                    return Ok(());
142                }
143                if meta.path.is_ident("rename_all") {
144                    let value = meta.value()?.parse::<LitStr>()?;
145                    out.rename_all = Some(value.value());
146                    return Ok(());
147                }
148                if meta.path.is_ident("description") {
149                    let value = meta.value()?.parse::<LitStr>()?;
150                    out.description = Some(value.value());
151                    return Ok(());
152                }
153                if meta.path.is_ident("example") {
154                    let value = meta.value()?.parse::<Lit>()?;
155                    out.example = Some(value);
156                    return Ok(());
157                }
158                Err(meta.error("unsupported generable attribute"))
159            })?;
160        }
161
162        if attr.path().is_ident("serde") {
163            attr.parse_nested_meta(|meta| {
164                if meta.path.is_ident("rename_all") {
165                    let value = meta.value()?.parse::<LitStr>()?;
166                    out.rename_all = Some(value.value());
167                    return Ok(());
168                }
169                Ok(())
170            })?;
171        }
172    }
173
174    if out.description.is_none() {
175        out.description = doc_comment(attrs);
176    }
177
178    Ok(out)
179}
180
181fn parse_field_attrs(attrs: &[Attribute]) -> syn::Result<FieldAttrs> {
182    let mut out = FieldAttrs::default();
183
184    for attr in attrs {
185        if attr.path().is_ident("generable") {
186            attr.parse_nested_meta(|meta| {
187                if meta.path.is_ident("rename") {
188                    let value = meta.value()?.parse::<LitStr>()?;
189                    out.rename = Some(value.value());
190                    return Ok(());
191                }
192                if meta.path.is_ident("skip") {
193                    out.skip = true;
194                    return Ok(());
195                }
196                if meta.path.is_ident("description") {
197                    let value = meta.value()?.parse::<LitStr>()?;
198                    out.description = Some(value.value());
199                    return Ok(());
200                }
201                if meta.path.is_ident("example") {
202                    let value = meta.value()?.parse::<Lit>()?;
203                    out.example = Some(value);
204                    return Ok(());
205                }
206                if meta.path.is_ident("minimum") {
207                    let value = meta.value()?.parse::<Lit>()?;
208                    out.minimum = Some(value);
209                    return Ok(());
210                }
211                if meta.path.is_ident("maximum") {
212                    let value = meta.value()?.parse::<Lit>()?;
213                    out.maximum = Some(value);
214                    return Ok(());
215                }
216                if meta.path.is_ident("min_length") {
217                    let value = meta.value()?.parse::<Lit>()?;
218                    out.min_length = Some(value);
219                    return Ok(());
220                }
221                if meta.path.is_ident("max_length") {
222                    let value = meta.value()?.parse::<Lit>()?;
223                    out.max_length = Some(value);
224                    return Ok(());
225                }
226                if meta.path.is_ident("pattern") {
227                    let value = meta.value()?.parse::<LitStr>()?;
228                    out.pattern = Some(value.value());
229                    return Ok(());
230                }
231                if meta.path.is_ident("min_items") {
232                    let value = meta.value()?.parse::<Lit>()?;
233                    out.min_items = Some(value);
234                    return Ok(());
235                }
236                if meta.path.is_ident("max_items") {
237                    let value = meta.value()?.parse::<Lit>()?;
238                    out.max_items = Some(value);
239                    return Ok(());
240                }
241                if meta.path.is_ident("nullable") {
242                    let value = meta.value()?.parse::<LitBool>()?;
243                    out.nullable = value.value;
244                    return Ok(());
245                }
246                Err(meta.error("unsupported generable attribute"))
247            })?;
248        }
249
250        if attr.path().is_ident("serde") {
251            attr.parse_nested_meta(|meta| {
252                if meta.path.is_ident("rename") {
253                    let value = meta.value()?.parse::<LitStr>()?;
254                    out.rename = Some(value.value());
255                    return Ok(());
256                }
257                if meta.path.is_ident("skip") || meta.path.is_ident("skip_serializing") {
258                    out.skip = true;
259                    return Ok(());
260                }
261                if meta.path.is_ident("default") {
262                    out.default = true;
263                    return Ok(());
264                }
265                Ok(())
266            })?;
267        }
268    }
269
270    if out.description.is_none() {
271        out.description = doc_comment(attrs);
272    }
273
274    Ok(out)
275}
276
277fn schema_for_struct(
278    data: &DataStruct,
279    container: &ContainerAttrs,
280) -> syn::Result<proc_macro2::TokenStream> {
281    let crate_path = &container.crate_path;
282    let mut property_inserts = Vec::new();
283    let mut required_fields = Vec::new();
284    let mut container_inserts = Vec::new();
285
286    match &data.fields {
287        Fields::Named(fields) => {
288            for field in &fields.named {
289                let field_attrs = parse_field_attrs(&field.attrs)?;
290                if field_attrs.skip {
291                    continue;
292                }
293                let ident = field
294                    .ident
295                    .as_ref()
296                    .ok_or_else(|| syn::Error::new(field.span(), "expected named field"))?;
297                let name = field_name(ident.to_string(), &field_attrs, container);
298                let SchemaInfo {
299                    expr: schema_expr,
300                    optional,
301                } = schema_for_type(&field.ty, &field_attrs, crate_path)?;
302                property_inserts.push(quote! {
303                    properties.insert(#name.to_string(), #schema_expr);
304                });
305                if !optional && !field_attrs.default {
306                    required_fields.push(name);
307                }
308            }
309        }
310        Fields::Unnamed(fields) => {
311            if fields.unnamed.len() == 1 {
312                let field = fields
313                    .unnamed
314                    .first()
315                    .ok_or_else(|| syn::Error::new(fields.span(), "expected field"))?;
316                let field_attrs = parse_field_attrs(&field.attrs)?;
317                let schema_info = schema_for_type(&field.ty, &field_attrs, crate_path)?;
318                return Ok(schema_info.expr);
319            }
320            return Err(syn::Error::new(
321                fields.span(),
322                "Generable only supports named fields or newtype structs",
323            ));
324        }
325        Fields::Unit => {
326            return Err(syn::Error::new(
327                data.fields.span(),
328                "Generable does not support unit structs",
329            ));
330        }
331    }
332
333    let required_values = required_fields
334        .iter()
335        .map(|name| quote!(#crate_path::__serde_json::Value::String(#name.to_string())));
336
337    if let Some(desc) = &container.description {
338        let desc_lit = LitStr::new(desc, proc_macro2::Span::call_site());
339        container_inserts.push(quote! {
340            schema.insert(
341                "description".to_string(),
342                #crate_path::__serde_json::Value::String(#desc_lit.to_string())
343            );
344        });
345    }
346    if let Some(example) = &container.example {
347        let example_tokens = example.to_token_stream();
348        container_inserts.push(quote! {
349            schema.insert(
350                "example".to_string(),
351                #crate_path::__serde_json::json!(#example_tokens)
352            );
353        });
354    }
355
356    Ok(quote! {
357        let mut schema = #crate_path::__serde_json::Map::new();
358        schema.insert(
359            "type".to_string(),
360            #crate_path::__serde_json::Value::String("object".to_string())
361        );
362        let mut properties = #crate_path::__serde_json::Map::new();
363        #(#property_inserts)*
364        schema.insert(
365            "properties".to_string(),
366            #crate_path::__serde_json::Value::Object(properties)
367        );
368        let mut required = Vec::new();
369        #(required.push(#required_values);)*
370        if !required.is_empty() {
371            schema.insert("required".to_string(), #crate_path::__serde_json::Value::Array(required));
372        }
373        #(#container_inserts)*
374        #crate_path::__serde_json::Value::Object(schema)
375    })
376}
377
378fn schema_for_enum(
379    data: &DataEnum,
380    container: &ContainerAttrs,
381) -> syn::Result<proc_macro2::TokenStream> {
382    let crate_path = &container.crate_path;
383    let mut variants = Vec::new();
384    let mut container_inserts = Vec::new();
385
386    for variant in &data.variants {
387        match &variant.fields {
388            Fields::Unit => {}
389            _ => {
390                return Err(syn::Error::new(
391                    variant.span(),
392                    "Generable only supports unit enums (string-only)",
393                ));
394            }
395        }
396
397        let variant_attrs = parse_field_attrs(&variant.attrs)?;
398        if variant_attrs.skip {
399            continue;
400        }
401        let variant_name = apply_rename(variant.ident.to_string(), &variant_attrs, container);
402        variants.push(variant_name);
403    }
404
405    let variant_values = variants
406        .iter()
407        .map(|name| quote!(#crate_path::__serde_json::Value::String(#name.to_string())));
408
409    if let Some(desc) = &container.description {
410        let desc_lit = LitStr::new(desc, proc_macro2::Span::call_site());
411        container_inserts.push(quote! {
412            schema.insert(
413                "description".to_string(),
414                #crate_path::__serde_json::Value::String(#desc_lit.to_string())
415            );
416        });
417    }
418    if let Some(example) = &container.example {
419        let example_tokens = example.to_token_stream();
420        container_inserts.push(quote! {
421            schema.insert(
422                "example".to_string(),
423                #crate_path::__serde_json::json!(#example_tokens)
424            );
425        });
426    }
427
428    Ok(quote! {
429        let mut schema = #crate_path::__serde_json::Map::new();
430        let variants = vec![#(#variant_values),*];
431        schema.insert(
432            "type".to_string(),
433            #crate_path::__serde_json::Value::String("string".to_string())
434        );
435        schema.insert("enum".to_string(), #crate_path::__serde_json::Value::Array(variants));
436        #(#container_inserts)*
437        #crate_path::__serde_json::Value::Object(schema)
438    })
439}
440
441fn schema_for_type(ty: &Type, attrs: &FieldAttrs, crate_path: &Path) -> syn::Result<SchemaInfo> {
442    if let Some(inner) = option_inner(ty) {
443        let mut schema = schema_for_type(inner, attrs, crate_path)?;
444        schema.optional = true;
445        return Ok(schema);
446    }
447
448    if let Some(inner) = vec_inner(ty) {
449        let inner_schema = schema_for_type(inner, &FieldAttrs::default(), crate_path)?.expr;
450        let mut entries = vec![schema_type_entry(crate_path, "array")];
451        entries.push(schema_entry("items", quote!(#inner_schema)));
452        add_attr_entries(crate_path, attrs, &mut entries);
453        return Ok(SchemaInfo {
454            expr: schema_object_expr(crate_path, entries),
455            optional: false,
456        });
457    }
458
459    if let Some((key_ty, value_ty)) = map_inner(ty) {
460        if !is_string_type(key_ty) {
461            return Err(syn::Error::new(
462                key_ty.span(),
463                "Generable only supports map keys of type String",
464            ));
465        }
466        let value_schema = schema_for_type(value_ty, &FieldAttrs::default(), crate_path)?.expr;
467        let mut entries = vec![schema_type_entry(crate_path, "object")];
468        entries.push(schema_entry("additionalProperties", quote!(#value_schema)));
469        add_attr_entries(crate_path, attrs, &mut entries);
470        return Ok(SchemaInfo {
471            expr: schema_object_expr(crate_path, entries),
472            optional: false,
473        });
474    }
475
476    if is_string_type(ty) {
477        let mut entries = vec![schema_type_entry(crate_path, "string")];
478        add_attr_entries(crate_path, attrs, &mut entries);
479        return Ok(SchemaInfo {
480            expr: schema_object_expr(crate_path, entries),
481            optional: false,
482        });
483    }
484
485    if is_bool_type(ty) {
486        let mut entries = vec![schema_type_entry(crate_path, "boolean")];
487        add_attr_entries(crate_path, attrs, &mut entries);
488        return Ok(SchemaInfo {
489            expr: schema_object_expr(crate_path, entries),
490            optional: false,
491        });
492    }
493
494    if is_integer_type(ty) {
495        let mut entries = vec![schema_type_entry(crate_path, "integer")];
496        add_attr_entries(crate_path, attrs, &mut entries);
497        return Ok(SchemaInfo {
498            expr: schema_object_expr(crate_path, entries),
499            optional: false,
500        });
501    }
502
503    if is_number_type(ty) {
504        let mut entries = vec![schema_type_entry(crate_path, "number")];
505        add_attr_entries(crate_path, attrs, &mut entries);
506        return Ok(SchemaInfo {
507            expr: schema_object_expr(crate_path, entries),
508            optional: false,
509        });
510    }
511
512    if is_serde_json_value(ty) {
513        let mut entries = Vec::new();
514        add_attr_entries(crate_path, attrs, &mut entries);
515        return Ok(SchemaInfo {
516            expr: schema_object_expr(crate_path, entries),
517            optional: false,
518        });
519    }
520
521    let mut entries = Vec::new();
522    add_attr_entries(crate_path, attrs, &mut entries);
523    let schema = if entries.is_empty() {
524        quote!(<#ty as #crate_path::Generable>::schema())
525    } else {
526        let base = quote!(<#ty as #crate_path::Generable>::schema());
527        quote!({
528            let mut schema = #base;
529            if let #crate_path::__serde_json::Value::Object(ref mut map) = schema {
530                #(#entries)*
531            }
532            schema
533        })
534    };
535
536    Ok(SchemaInfo {
537        expr: schema,
538        optional: false,
539    })
540}
541
542fn schema_object_expr(
543    crate_path: &Path,
544    entries: Vec<proc_macro2::TokenStream>,
545) -> proc_macro2::TokenStream {
546    quote!({
547        let mut map = #crate_path::__serde_json::Map::new();
548        #(#entries)*
549        #crate_path::__serde_json::Value::Object(map)
550    })
551}
552
553fn schema_type_entry(crate_path: &Path, ty: &str) -> proc_macro2::TokenStream {
554    let lit = LitStr::new(ty, proc_macro2::Span::call_site());
555    schema_entry(
556        "type",
557        quote!(#crate_path::__serde_json::Value::String(#lit.to_string())),
558    )
559}
560
561fn schema_entry(key: &str, value_expr: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
562    let lit = LitStr::new(key, proc_macro2::Span::call_site());
563    quote!(map.insert(#lit.to_string(), #value_expr);)
564}
565
566fn schema_string_insert(crate_path: &Path, key: &str, value: &str) -> proc_macro2::TokenStream {
567    let key_lit = LitStr::new(key, proc_macro2::Span::call_site());
568    let val_lit = LitStr::new(value, proc_macro2::Span::call_site());
569    quote!(map.insert(#key_lit.to_string(), #crate_path::__serde_json::Value::String(#val_lit.to_string()));)
570}
571
572fn schema_value_insert(crate_path: &Path, key: &str, value: &Lit) -> proc_macro2::TokenStream {
573    let key_lit = LitStr::new(key, proc_macro2::Span::call_site());
574    let value_tokens = value.to_token_stream();
575    quote!(map.insert(#key_lit.to_string(), #crate_path::__serde_json::json!(#value_tokens));)
576}
577
578fn add_attr_entries(
579    crate_path: &Path,
580    attrs: &FieldAttrs,
581    entries: &mut Vec<proc_macro2::TokenStream>,
582) {
583    if let Some(desc) = &attrs.description {
584        entries.push(schema_string_insert(crate_path, "description", desc));
585    }
586    if let Some(example) = &attrs.example {
587        entries.push(schema_value_insert(crate_path, "example", example));
588    }
589    if let Some(minimum) = &attrs.minimum {
590        entries.push(schema_value_insert(crate_path, "minimum", minimum));
591    }
592    if let Some(maximum) = &attrs.maximum {
593        entries.push(schema_value_insert(crate_path, "maximum", maximum));
594    }
595    if let Some(min_length) = &attrs.min_length {
596        entries.push(schema_value_insert(crate_path, "minLength", min_length));
597    }
598    if let Some(max_length) = &attrs.max_length {
599        entries.push(schema_value_insert(crate_path, "maxLength", max_length));
600    }
601    if let Some(pattern) = &attrs.pattern {
602        entries.push(schema_string_insert(crate_path, "pattern", pattern));
603    }
604    if let Some(min_items) = &attrs.min_items {
605        entries.push(schema_value_insert(crate_path, "minItems", min_items));
606    }
607    if let Some(max_items) = &attrs.max_items {
608        entries.push(schema_value_insert(crate_path, "maxItems", max_items));
609    }
610    if attrs.nullable {
611        entries.push(schema_entry(
612            "nullable",
613            quote!(#crate_path::__serde_json::Value::Bool(true)),
614        ));
615    }
616}
617
618fn field_name(raw: String, attrs: &FieldAttrs, container: &ContainerAttrs) -> String {
619    if let Some(rename) = &attrs.rename {
620        return rename.clone();
621    }
622    if let Some(rule) = &container.rename_all {
623        apply_rename_all(&raw, rule)
624    } else {
625        raw
626    }
627}
628
629fn apply_rename(raw: String, attrs: &FieldAttrs, container: &ContainerAttrs) -> String {
630    if let Some(rename) = &attrs.rename {
631        return rename.clone();
632    }
633    if let Some(rule) = &container.rename_all {
634        apply_rename_all(&raw, rule)
635    } else {
636        raw
637    }
638}
639
640fn apply_rename_all(name: &str, rule: &str) -> String {
641    let words = split_words(name);
642    match rule {
643        "lowercase" => join_words(&words, "", |w| w.to_ascii_lowercase()),
644        "UPPERCASE" => join_words(&words, "", |w| w.to_ascii_uppercase()),
645        "snake_case" => join_words(&words, "_", |w| w.to_ascii_lowercase()),
646        "SCREAMING_SNAKE_CASE" => join_words(&words, "_", |w| w.to_ascii_uppercase()),
647        "kebab-case" => join_words(&words, "-", |w| w.to_ascii_lowercase()),
648        "PascalCase" => join_pascal(&words),
649        "camelCase" => join_camel(&words),
650        _ => name.to_string(),
651    }
652}
653
654fn split_words(name: &str) -> Vec<String> {
655    let mut words = Vec::new();
656    let mut current = String::new();
657    let mut chars = name.chars().peekable();
658    let mut prev_is_upper = false;
659    let mut prev_is_lower = false;
660
661    while let Some(ch) = chars.next() {
662        if ch == '_' || ch == '-' {
663            if !current.is_empty() {
664                words.push(current);
665                current = String::new();
666            }
667            prev_is_upper = false;
668            prev_is_lower = false;
669            continue;
670        }
671
672        let is_upper = ch.is_ascii_uppercase();
673        let is_lower = ch.is_ascii_lowercase();
674        let next_is_lower = chars
675            .peek()
676            .map(|next| next.is_ascii_lowercase())
677            .unwrap_or(false);
678
679        if !current.is_empty()
680            && ((prev_is_lower && is_upper) || (prev_is_upper && is_upper && next_is_lower))
681        {
682            words.push(current);
683            current = String::new();
684        }
685
686        current.push(ch);
687        prev_is_upper = is_upper;
688        prev_is_lower = is_lower;
689    }
690
691    if !current.is_empty() {
692        words.push(current);
693    }
694
695    words
696}
697
698fn join_words<F>(words: &[String], sep: &str, mut transform: F) -> String
699where
700    F: FnMut(&str) -> String,
701{
702    let mut out = String::new();
703    for (idx, word) in words.iter().enumerate() {
704        if idx > 0 {
705            out.push_str(sep);
706        }
707        out.push_str(&transform(word));
708    }
709    out
710}
711
712fn join_pascal(words: &[String]) -> String {
713    let mut out = String::new();
714    for word in words {
715        if word.is_empty() {
716            continue;
717        }
718        let mut chars = word.chars();
719        if let Some(first) = chars.next() {
720            out.push(first.to_ascii_uppercase());
721            out.push_str(&chars.as_str().to_ascii_lowercase());
722        }
723    }
724    out
725}
726
727fn join_camel(words: &[String]) -> String {
728    let mut out = String::new();
729    for (idx, word) in words.iter().enumerate() {
730        if word.is_empty() {
731            continue;
732        }
733        if idx == 0 {
734            out.push_str(&word.to_ascii_lowercase());
735        } else {
736            out.push_str(&join_pascal(std::slice::from_ref(word)));
737        }
738    }
739    out
740}
741
742fn option_inner(ty: &Type) -> Option<&Type> {
743    if let Type::Path(type_path) = ty {
744        let segment = type_path.path.segments.last()?;
745        if segment.ident == "Option"
746            && let syn::PathArguments::AngleBracketed(args) = &segment.arguments
747            && let Some(syn::GenericArgument::Type(inner)) = args.args.first()
748        {
749            return Some(inner);
750        }
751    }
752    None
753}
754
755fn vec_inner(ty: &Type) -> Option<&Type> {
756    if let Type::Path(type_path) = ty {
757        let segment = type_path.path.segments.last()?;
758        if (segment.ident == "Vec" || segment.ident == "VecDeque")
759            && let syn::PathArguments::AngleBracketed(args) = &segment.arguments
760            && let Some(syn::GenericArgument::Type(inner)) = args.args.first()
761        {
762            return Some(inner);
763        }
764    }
765    None
766}
767
768fn map_inner(ty: &Type) -> Option<(&Type, &Type)> {
769    if let Type::Path(type_path) = ty {
770        let segment = type_path.path.segments.last()?;
771        if (segment.ident == "HashMap" || segment.ident == "BTreeMap")
772            && let syn::PathArguments::AngleBracketed(args) = &segment.arguments
773        {
774            let mut iter = args.args.iter();
775            let key = match iter.next()? {
776                syn::GenericArgument::Type(ty) => ty,
777                _ => return None,
778            };
779            let value = match iter.next()? {
780                syn::GenericArgument::Type(ty) => ty,
781                _ => return None,
782            };
783            return Some((key, value));
784        }
785    }
786    None
787}
788
789fn is_string_type(ty: &Type) -> bool {
790    match ty {
791        Type::Path(type_path) => {
792            let ident = &type_path.path.segments.last().map(|s| &s.ident);
793            matches!(ident, Some(id) if *id == "String")
794        }
795        Type::Reference(reference) => match &*reference.elem {
796            Type::Path(type_path) => {
797                let ident = &type_path.path.segments.last().map(|s| &s.ident);
798                matches!(ident, Some(id) if *id == "str")
799            }
800            _ => false,
801        },
802        _ => false,
803    }
804}
805
806fn is_bool_type(ty: &Type) -> bool {
807    matches!(ty, Type::Path(type_path) if type_path.path.is_ident("bool"))
808}
809
810fn is_integer_type(ty: &Type) -> bool {
811    if let Type::Path(type_path) = ty {
812        let ident = type_path.path.segments.last().map(|s| s.ident.to_string());
813        if let Some(name) = ident {
814            return matches!(
815                name.as_str(),
816                "u8" | "u16"
817                    | "u32"
818                    | "u64"
819                    | "u128"
820                    | "usize"
821                    | "i8"
822                    | "i16"
823                    | "i32"
824                    | "i64"
825                    | "i128"
826                    | "isize"
827            );
828        }
829    }
830    false
831}
832
833fn is_number_type(ty: &Type) -> bool {
834    matches!(ty, Type::Path(type_path) if type_path.path.is_ident("f32") || type_path.path.is_ident("f64"))
835}
836
837fn is_serde_json_value(ty: &Type) -> bool {
838    if let Type::Path(type_path) = ty
839        && let Some(last) = type_path.path.segments.last()
840    {
841        return last.ident == "Value";
842    }
843    false
844}
845
846fn doc_comment(attrs: &[Attribute]) -> Option<String> {
847    let mut parts = Vec::new();
848    for attr in attrs {
849        if attr.path().is_ident("doc")
850            && let Meta::NameValue(meta) = &attr.meta
851            && let syn::Expr::Lit(expr_lit) = &meta.value
852            && let Lit::Str(lit) = &expr_lit.lit
853        {
854            let value = lit.value();
855            let trimmed = value.trim();
856            if !trimmed.is_empty() {
857                parts.push(trimmed.to_string());
858            }
859        }
860    }
861    if parts.is_empty() {
862        None
863    } else {
864        Some(parts.join(" "))
865    }
866}