Skip to main content

fig_macros/
lib.rs

1//! Derive macros for the `fig` crate's `ToValue`/`FromValue` traits.
2//!
3//! These generate straight-line conversions to and from `fig::Value` — no
4//! format-generic visitor machinery, so the emitted code stays small. The
5//! macros are re-exported from `fig` behind its `derive` feature; depend on
6//! `fig`, not on this crate directly.
7//!
8//! # Structs
9//! Named-field, newtype (one field), and unit structs. Field attributes:
10//! `#[fig(rename = "..")]`, `#[fig(skip)]`, `#[fig(flatten)]`, `#[fig(default)]`
11//! or `#[fig(default = "path")]` (call `path()` for a missing key),
12//! `#[fig(skip_serializing_if = "path")]` (omit from `ToValue` output when the
13//! predicate `fn(&Field) -> bool` is true), and `#[fig(deserialize_with =
14//! "path")]` (parse a present value with `path(&fig::Value) -> Result<Field,
15//! fig::Error>`). `Option<_>` fields are optional
16//! (absent key → `None`). The container attribute `#[fig(rename_all = "..")]`
17//! applies a case rule (`camelCase`, `snake_case`, `PascalCase`, `kebab-case`,
18//! and their SCREAMING variants, plus `lowercase`/`UPPERCASE`) to every field
19//! name not carrying an explicit `rename`.
20//!
21//! # Enums
22//! All four serde-style taggings, chosen by container attribute:
23//! * external (default) — `"Variant"` / `{ "Variant": <content> }`
24//! * internal — `#[fig(tag = "type")]` → `{ "type": "Variant", ..fields }`
25//! * adjacent — `#[fig(tag = "type", content = "data")]`
26//! * untagged — `#[fig(untagged)]` (first matching variant wins, in order)
27//!
28//! Variant shapes: unit, newtype, tuple, struct. Variant `#[fig(rename = "..")]`
29//! is honored, and the container `#[fig(rename_all = "..")]` applies to variant
30//! names (matching serde — it does not rename a struct-variant's inner fields).
31//! Restrictions (matching/extending serde): tuple variants are not allowed with
32//! internal tagging, and `#[fig(flatten)]` is not yet supported inside enum
33//! variants.
34
35use proc_macro::TokenStream;
36use proc_macro2::TokenStream as TokenStream2;
37use quote::quote;
38use syn::{
39    Data, DeriveInput, Fields, FieldsNamed, Generics, Ident, LitStr, Type, Variant,
40    parse_macro_input,
41};
42
43#[proc_macro_derive(ToValue, attributes(fig))]
44pub fn derive_to_value(input: TokenStream) -> TokenStream {
45    let input = parse_macro_input!(input as DeriveInput);
46    expand_to_value(&input)
47        .unwrap_or_else(syn::Error::into_compile_error)
48        .into()
49}
50
51#[proc_macro_derive(FromValue, attributes(fig))]
52pub fn derive_from_value(input: TokenStream) -> TokenStream {
53    let input = parse_macro_input!(input as DeriveInput);
54    expand_from_value(&input)
55        .unwrap_or_else(syn::Error::into_compile_error)
56        .into()
57}
58
59// ============================================================================
60// Attribute parsing
61// ============================================================================
62
63/// Case-conversion for `#[fig(rename_all = "..")]`, matching serde's rules.
64///
65/// Field names are assumed snake_case and variant names PascalCase, so the two
66/// `apply_to_*` methods differ exactly as serde's do — this keeps generated keys
67/// byte-identical to serde output (and therefore to ts-rs bindings).
68#[derive(Clone, Copy)]
69enum RenameRule {
70    Lower,
71    Upper,
72    Pascal,
73    Camel,
74    Snake,
75    ScreamingSnake,
76    Kebab,
77    ScreamingKebab,
78}
79
80impl RenameRule {
81    fn from_str(s: &str) -> Result<Self, String> {
82        Ok(match s {
83            "lowercase" => RenameRule::Lower,
84            "UPPERCASE" => RenameRule::Upper,
85            "PascalCase" => RenameRule::Pascal,
86            "camelCase" => RenameRule::Camel,
87            "snake_case" => RenameRule::Snake,
88            "SCREAMING_SNAKE_CASE" => RenameRule::ScreamingSnake,
89            "kebab-case" => RenameRule::Kebab,
90            "SCREAMING-KEBAB-CASE" => RenameRule::ScreamingKebab,
91            other => {
92                return Err(format!(
93                    "unknown `rename_all` rule `{other}` (expected one of: lowercase, \
94                     UPPERCASE, PascalCase, camelCase, snake_case, SCREAMING_SNAKE_CASE, \
95                     kebab-case, SCREAMING-KEBAB-CASE)"
96                ));
97            }
98        })
99    }
100
101    /// Apply to a snake_case field name.
102    fn apply_to_field(self, field: &str) -> String {
103        match self {
104            RenameRule::Lower | RenameRule::Snake => field.to_owned(),
105            RenameRule::Upper | RenameRule::ScreamingSnake => field.to_ascii_uppercase(),
106            RenameRule::Pascal => {
107                let mut out = String::new();
108                let mut capitalize = true;
109                for ch in field.chars() {
110                    if ch == '_' {
111                        capitalize = true;
112                    } else if capitalize {
113                        out.push(ch.to_ascii_uppercase());
114                        capitalize = false;
115                    } else {
116                        out.push(ch);
117                    }
118                }
119                out
120            }
121            RenameRule::Camel => {
122                let pascal = RenameRule::Pascal.apply_to_field(field);
123                match pascal.char_indices().nth(1) {
124                    Some((i, _)) => pascal[..1].to_ascii_lowercase() + &pascal[i..],
125                    None => pascal.to_ascii_lowercase(),
126                }
127            }
128            RenameRule::Kebab => field.replace('_', "-"),
129            RenameRule::ScreamingKebab => field.to_ascii_uppercase().replace('_', "-"),
130        }
131    }
132
133    /// Apply to a PascalCase variant name.
134    fn apply_to_variant(self, variant: &str) -> String {
135        match self {
136            RenameRule::Pascal => variant.to_owned(),
137            RenameRule::Lower => variant.to_ascii_lowercase(),
138            RenameRule::Upper => variant.to_ascii_uppercase(),
139            RenameRule::Camel => match variant.char_indices().nth(1) {
140                Some((i, _)) => variant[..1].to_ascii_lowercase() + &variant[i..],
141                None => variant.to_ascii_lowercase(),
142            },
143            RenameRule::Snake => {
144                let mut out = String::new();
145                for (i, ch) in variant.char_indices() {
146                    if i > 0 && ch.is_uppercase() {
147                        out.push('_');
148                    }
149                    out.push(ch.to_ascii_lowercase());
150                }
151                out
152            }
153            RenameRule::ScreamingSnake => {
154                RenameRule::Snake.apply_to_variant(variant).to_ascii_uppercase()
155            }
156            RenameRule::Kebab => RenameRule::Snake.apply_to_variant(variant).replace('_', "-"),
157            RenameRule::ScreamingKebab => RenameRule::ScreamingSnake
158                .apply_to_variant(variant)
159                .replace('_', "-"),
160        }
161    }
162}
163
164/// Parsed `#[fig(..)]` attributes on a single field.
165#[derive(Default)]
166struct FieldAttrs {
167    rename: Option<String>,
168    skip: bool,
169    flatten: bool,
170    default: bool,
171    /// `#[fig(default = "path")]` — call `path()` for a missing key instead of
172    /// `Default::default()`. Mirrors serde's `default = ".."`.
173    default_path: Option<syn::Path>,
174    /// `#[fig(skip_serializing_if = "path")]` — predicate `fn(&Field) -> bool`
175    /// that, when true, omits the field from `ToValue` output.
176    skip_serializing_if: Option<syn::Path>,
177    /// `#[fig(deserialize_with = "path")]` — parse a present value with
178    /// `path(&fig::Value) -> Result<Field, fig::Error>` instead of the field
179    /// type's `FromValue`. Mirrors serde's `deserialize_with`.
180    deserialize_with: Option<syn::Path>,
181    /// `#[fig(alias = "old")]` — additional key(s) accepted when reading, tried
182    /// in order after the primary key. Mirrors serde's `alias`.
183    aliases: Vec<String>,
184}
185
186fn parse_field_attrs(attrs: &[syn::Attribute]) -> syn::Result<FieldAttrs> {
187    let mut parsed = FieldAttrs::default();
188    for attr in attrs {
189        if !attr.path().is_ident("fig") {
190            continue;
191        }
192        attr.parse_nested_meta(|meta| {
193            if meta.path.is_ident("rename") {
194                parsed.rename = Some(meta.value()?.parse::<LitStr>()?.value());
195            } else if meta.path.is_ident("skip") {
196                parsed.skip = true;
197            } else if meta.path.is_ident("flatten") {
198                parsed.flatten = true;
199            } else if meta.path.is_ident("default") {
200                // Bare `default` (use `Default`) or `default = "path"` (call fn).
201                if let Ok(value) = meta.value() {
202                    parsed.default_path = Some(value.parse::<LitStr>()?.parse::<syn::Path>()?);
203                } else {
204                    parsed.default = true;
205                }
206            } else if meta.path.is_ident("skip_serializing_if") {
207                let path = meta.value()?.parse::<LitStr>()?.parse::<syn::Path>()?;
208                parsed.skip_serializing_if = Some(path);
209            } else if meta.path.is_ident("deserialize_with") {
210                let path = meta.value()?.parse::<LitStr>()?.parse::<syn::Path>()?;
211                parsed.deserialize_with = Some(path);
212            } else if meta.path.is_ident("alias") {
213                parsed.aliases.push(meta.value()?.parse::<LitStr>()?.value());
214            } else {
215                return Err(meta.error(
216                    "unknown `fig` field attribute (expected: rename, skip, flatten, \
217                     default, skip_serializing_if, deserialize_with, alias)",
218                ));
219            }
220            Ok(())
221        })?;
222    }
223    Ok(parsed)
224}
225
226/// Parsed `#[fig(..)]` attributes on a variant.
227#[derive(Default)]
228struct VariantAttrs {
229    rename: Option<String>,
230}
231
232fn parse_variant_attrs(attrs: &[syn::Attribute]) -> syn::Result<VariantAttrs> {
233    let mut parsed = VariantAttrs::default();
234    for attr in attrs {
235        if !attr.path().is_ident("fig") {
236            continue;
237        }
238        attr.parse_nested_meta(|meta| {
239            if meta.path.is_ident("rename") {
240                parsed.rename = Some(meta.value()?.parse::<LitStr>()?.value());
241            } else {
242                return Err(meta.error("unknown `fig` variant attribute (expected: rename)"));
243            }
244            Ok(())
245        })?;
246    }
247    Ok(parsed)
248}
249
250/// Parsed `#[fig(..)]` attributes on a struct or enum container.
251#[derive(Default)]
252struct ContainerAttrs {
253    rename_all: Option<RenameRule>,
254    tag: Option<String>,
255    content: Option<String>,
256    untagged: bool,
257}
258
259fn parse_container_attrs(attrs: &[syn::Attribute]) -> syn::Result<ContainerAttrs> {
260    let mut parsed = ContainerAttrs::default();
261    for attr in attrs {
262        if !attr.path().is_ident("fig") {
263            continue;
264        }
265        attr.parse_nested_meta(|meta| {
266            if meta.path.is_ident("rename_all") {
267                let lit = meta.value()?.parse::<LitStr>()?;
268                let rule = RenameRule::from_str(&lit.value())
269                    .map_err(|msg| syn::Error::new(lit.span(), msg))?;
270                parsed.rename_all = Some(rule);
271            } else if meta.path.is_ident("tag") {
272                parsed.tag = Some(meta.value()?.parse::<LitStr>()?.value());
273            } else if meta.path.is_ident("content") {
274                parsed.content = Some(meta.value()?.parse::<LitStr>()?.value());
275            } else if meta.path.is_ident("untagged") {
276                parsed.untagged = true;
277            } else {
278                return Err(meta.error(
279                    "unknown `fig` container attribute (expected: rename_all, tag, content, untagged)",
280                ));
281            }
282            Ok(())
283        })?;
284    }
285    Ok(parsed)
286}
287
288/// The container-level `rename_all` rule, if any.
289fn container_rename_all(attrs: &[syn::Attribute]) -> syn::Result<Option<RenameRule>> {
290    Ok(parse_container_attrs(attrs)?.rename_all)
291}
292
293/// How an enum's variants are distinguished on the wire.
294enum Tagging {
295    External,
296    Internal(String),
297    Adjacent(String, String),
298    Untagged,
299}
300
301fn tagging_of(input: &DeriveInput) -> syn::Result<Tagging> {
302    let c = parse_container_attrs(&input.attrs)?;
303    match (c.untagged, c.tag, c.content) {
304        (true, None, None) => Ok(Tagging::Untagged),
305        (true, _, _) => Err(syn::Error::new_spanned(
306            input,
307            "`#[fig(untagged)]` cannot be combined with `tag`/`content`",
308        )),
309        (false, Some(tag), Some(content)) => Ok(Tagging::Adjacent(tag, content)),
310        (false, Some(tag), None) => Ok(Tagging::Internal(tag)),
311        (false, None, Some(_)) => Err(syn::Error::new_spanned(
312            input,
313            "`#[fig(content = ..)]` requires `#[fig(tag = ..)]`",
314        )),
315        (false, None, None) => Ok(Tagging::External),
316    }
317}
318
319// ============================================================================
320// Shared field model
321// ============================================================================
322
323struct FieldInfo<'a> {
324    ident: &'a Ident,
325    ty: &'a Type,
326    /// Serialized key (rename or field name). Unused for flattened fields.
327    key: String,
328    skip: bool,
329    flatten: bool,
330    /// Whether a missing key falls back to a default instead of erroring.
331    use_default: bool,
332    /// `default = "path"` function to call for a missing key (overrides the
333    /// plain `Default::default()` fallback).
334    default_path: Option<syn::Path>,
335    /// `skip_serializing_if` predicate path, omitting the field from output
336    /// when it returns `true`.
337    skip_serializing_if: Option<syn::Path>,
338    /// `deserialize_with` function to parse a present value.
339    deserialize_with: Option<syn::Path>,
340    /// Alternate keys accepted when reading (after the primary key).
341    aliases: Vec<String>,
342}
343
344/// Collect a struct/variant's named fields. `rename_all` (the container rule, if
345/// any) is applied to each field name unless the field carries an explicit
346/// `#[fig(rename = "..")]`, which always wins.
347fn collect_named_fields(
348    fields: &FieldsNamed,
349    rename_all: Option<RenameRule>,
350) -> syn::Result<Vec<FieldInfo<'_>>> {
351    let mut infos = Vec::with_capacity(fields.named.len());
352    for field in &fields.named {
353        let attrs = parse_field_attrs(&field.attrs)?;
354        if attrs.flatten && attrs.rename.is_some() {
355            return Err(syn::Error::new_spanned(
356                field,
357                "`#[fig(flatten)]` and `#[fig(rename)]` are mutually exclusive",
358            ));
359        }
360        let ident = field.ident.as_ref().expect("named field has an ident");
361        let key = match attrs.rename {
362            Some(explicit) => explicit,
363            None => match rename_all {
364                Some(rule) => rule.apply_to_field(&ident.to_string()),
365                None => ident.to_string(),
366            },
367        };
368        let use_default = attrs.default || attrs.default_path.is_some() || is_option(&field.ty);
369        infos.push(FieldInfo {
370            ident,
371            ty: &field.ty,
372            key,
373            skip: attrs.skip,
374            flatten: attrs.flatten,
375            use_default,
376            default_path: attrs.default_path,
377            skip_serializing_if: attrs.skip_serializing_if,
378            deserialize_with: attrs.deserialize_with,
379            aliases: attrs.aliases,
380        });
381    }
382    Ok(infos)
383}
384
385/// Heuristic: does this type's final path segment read as `Option`? Good enough
386/// to make `Option<T>` fields optional without an explicit `#[fig(default)]`.
387fn is_option(ty: &Type) -> bool {
388    matches!(ty, Type::Path(tp) if tp.qself.is_none()
389        && tp.path.segments.last().is_some_and(|s| s.ident == "Option"))
390}
391
392/// Rebuild the where-clause adding `T: <bound>` for every generic type param.
393fn bounded_where(generics: &Generics, bound: TokenStream2) -> TokenStream2 {
394    let mut preds: Vec<TokenStream2> = Vec::new();
395    if let Some(existing) = &generics.where_clause {
396        for p in &existing.predicates {
397            preds.push(quote!(#p));
398        }
399    }
400    for tp in generics.type_params() {
401        let id = &tp.ident;
402        preds.push(quote!(#id: #bound));
403    }
404    if preds.is_empty() {
405        quote!()
406    } else {
407        quote!(where #(#preds),*)
408    }
409}
410
411/// The wire key for an enum variant. An explicit `#[fig(rename = "..")]` wins;
412/// otherwise the container `rename_all` rule (if any) is applied.
413fn variant_key(variant: &Variant, rename_all: Option<RenameRule>) -> syn::Result<String> {
414    let attrs = parse_variant_attrs(&variant.attrs)?;
415    Ok(match attrs.rename {
416        Some(explicit) => explicit,
417        None => match rename_all {
418            Some(rule) => rule.apply_to_variant(&variant.ident.to_string()),
419            None => variant.ident.to_string(),
420        },
421    })
422}
423
424// ============================================================================
425// ToValue
426// ============================================================================
427
428fn expand_to_value(input: &DeriveInput) -> syn::Result<TokenStream2> {
429    let name = &input.ident;
430    let (impl_g, ty_g, _) = input.generics.split_for_impl();
431    let where_clause = bounded_where(&input.generics, quote!(fig::ToValue));
432
433    let body = match &input.data {
434        Data::Struct(s) => to_value_struct(&s.fields, input)?,
435        Data::Enum(e) => to_value_enum(input, e)?,
436        Data::Union(_) => {
437            return Err(syn::Error::new_spanned(
438                input,
439                "fig's ToValue derive does not support unions",
440            ));
441        }
442    };
443
444    Ok(quote! {
445        impl #impl_g fig::ToValue for #name #ty_g #where_clause {
446            fn to_value(&self) -> fig::Value {
447                #body
448            }
449        }
450    })
451}
452
453fn to_value_struct(fields: &Fields, input: &DeriveInput) -> syn::Result<TokenStream2> {
454    match fields {
455        Fields::Named(named) => {
456            let infos = collect_named_fields(named, container_rename_all(&input.attrs)?)?;
457            let stmts = infos.iter().filter(|f| !f.skip).map(|f| {
458                let ident = f.ident;
459                if f.flatten {
460                    quote! {
461                        if let fig::Value::Map(mut __m) = fig::ToValue::to_value(&self.#ident) {
462                            __entries.append(&mut __m);
463                        }
464                    }
465                } else {
466                    let key = &f.key;
467                    let push = quote! {
468                        __entries.push((
469                            fig::Value::Str(::std::string::String::from(#key)),
470                            fig::ToValue::to_value(&self.#ident),
471                        ));
472                    };
473                    match &f.skip_serializing_if {
474                        Some(pred) => quote! {
475                            if !#pred(&self.#ident) { #push }
476                        },
477                        None => push,
478                    }
479                }
480            });
481            Ok(quote! {
482                let mut __entries: ::std::vec::Vec<(fig::Value, fig::Value)> = ::std::vec::Vec::new();
483                #(#stmts)*
484                fig::Value::Map(__entries)
485            })
486        }
487        Fields::Unnamed(unnamed) if unnamed.unnamed.len() == 1 => {
488            Ok(quote! { fig::ToValue::to_value(&self.0) })
489        }
490        Fields::Unnamed(_) => Err(syn::Error::new_spanned(
491            input,
492            "fig's ToValue derive supports newtype structs (one field) but not multi-field tuple structs yet",
493        )),
494        Fields::Unit => Ok(quote! { fig::Value::Null }),
495    }
496}
497
498fn to_value_enum(input: &DeriveInput, data: &syn::DataEnum) -> syn::Result<TokenStream2> {
499    let tagging = tagging_of(input)?;
500    let rename_all = container_rename_all(&input.attrs)?;
501    let mut arms = Vec::with_capacity(data.variants.len());
502    for variant in &data.variants {
503        arms.push(to_value_variant_arm(variant, &tagging, rename_all)?);
504    }
505    Ok(quote! {
506        match self {
507            #(#arms)*
508        }
509    })
510}
511
512/// Build one `match self` arm for an enum variant's `ToValue`.
513fn to_value_variant_arm(
514    variant: &Variant,
515    tagging: &Tagging,
516    rename_all: Option<RenameRule>,
517) -> syn::Result<TokenStream2> {
518    let vident = &variant.ident;
519    let key = variant_key(variant, rename_all)?;
520    let key_value = quote! { fig::Value::Str(::std::string::String::from(#key)) };
521
522    // Destructuring pattern + the "content" Value expression for non-unit shapes.
523    let (pattern, content): (TokenStream2, Option<TokenStream2>) = match &variant.fields {
524        Fields::Unit => (quote! { Self::#vident }, None),
525        Fields::Unnamed(u) if u.unnamed.len() == 1 => (
526            quote! { Self::#vident(__f0) },
527            Some(quote! { fig::ToValue::to_value(__f0) }),
528        ),
529        Fields::Unnamed(u) => {
530            let binds: Vec<Ident> = (0..u.unnamed.len())
531                .map(|i| Ident::new(&format!("__f{i}"), vident.span()))
532                .collect();
533            (
534                quote! { Self::#vident( #(#binds),* ) },
535                Some(quote! { fig::Value::Seq(vec![ #(fig::ToValue::to_value(#binds)),* ]) }),
536            )
537        }
538        Fields::Named(named) => {
539            // serde applies the container `rename_all` to variant *names*, not
540            // to a struct-variant's fields (that is serde's separate
541            // `rename_all_fields`, not implemented here), so pass `None`.
542            let infos = collect_named_fields(named, None)?;
543            if let Some(f) = infos.iter().find(|f| f.flatten) {
544                return Err(syn::Error::new_spanned(
545                    f.ident,
546                    "`#[fig(flatten)]` is not supported inside enum variants yet",
547                ));
548            }
549            let binds: Vec<Ident> = infos
550                .iter()
551                .map(|f| Ident::new(&format!("__f_{}", f.ident), f.ident.span()))
552                .collect();
553            let pat_fields = infos.iter().zip(&binds).map(|(f, b)| {
554                let id = f.ident;
555                quote! { #id: #b }
556            });
557            let entry_stmts = infos.iter().zip(&binds).filter(|(f, _)| !f.skip).map(|(f, b)| {
558                let fkey = &f.key;
559                let push = quote! {
560                    __vmap.push((
561                        fig::Value::Str(::std::string::String::from(#fkey)),
562                        fig::ToValue::to_value(#b),
563                    ));
564                };
565                match &f.skip_serializing_if {
566                    Some(pred) => quote! { if !#pred(#b) { #push } },
567                    None => push,
568                }
569            });
570            (
571                quote! { Self::#vident { #(#pat_fields),* } },
572                Some(quote! {{
573                    let mut __vmap: ::std::vec::Vec<(fig::Value, fig::Value)> =
574                        ::std::vec::Vec::new();
575                    #(#entry_stmts)*
576                    fig::Value::Map(__vmap)
577                }}),
578            )
579        }
580    };
581
582    let is_tuple_multi = matches!(&variant.fields, Fields::Unnamed(u) if u.unnamed.len() > 1);
583
584    let body = match tagging {
585        Tagging::External => match &content {
586            None => quote! { #key_value },
587            Some(c) => quote! { fig::Value::Map(vec![(#key_value, #c)]) },
588        },
589        Tagging::Adjacent(tag, content_key) => match &content {
590            None => quote! {
591                fig::Value::Map(vec![(
592                    fig::Value::Str(::std::string::String::from(#tag)),
593                    #key_value,
594                )])
595            },
596            Some(c) => quote! {
597                fig::Value::Map(vec![
598                    (fig::Value::Str(::std::string::String::from(#tag)), #key_value),
599                    (fig::Value::Str(::std::string::String::from(#content_key)), #c),
600                ])
601            },
602        },
603        Tagging::Internal(tag) => {
604            if is_tuple_multi {
605                return Err(syn::Error::new_spanned(
606                    variant,
607                    "internally tagged enums do not support tuple variants (matching serde); use adjacent or external tagging",
608                ));
609            }
610            match &content {
611                None => quote! {
612                    fig::Value::Map(vec![(
613                        fig::Value::Str(::std::string::String::from(#tag)),
614                        #key_value,
615                    )])
616                },
617                // struct/newtype: merge the content map alongside the tag. A
618                // newtype whose inner is not a mapping cannot be merged here
619                // (serde rejects it at runtime); we keep just the tag.
620                Some(c) => quote! {
621                    {
622                        let mut __entries: ::std::vec::Vec<(fig::Value, fig::Value)> = vec![(
623                            fig::Value::Str(::std::string::String::from(#tag)),
624                            #key_value,
625                        )];
626                        if let fig::Value::Map(mut __m) = #c {
627                            __entries.append(&mut __m);
628                        }
629                        fig::Value::Map(__entries)
630                    }
631                },
632            }
633        }
634        Tagging::Untagged => match &content {
635            None => quote! { fig::Value::Null },
636            Some(c) => quote! { #c },
637        },
638    };
639
640    Ok(quote! { #pattern => #body, })
641}
642
643// ============================================================================
644// FromValue
645// ============================================================================
646
647fn expand_from_value(input: &DeriveInput) -> syn::Result<TokenStream2> {
648    let name = &input.ident;
649    let (impl_g, ty_g, _) = input.generics.split_for_impl();
650    let where_clause = bounded_where(&input.generics, quote!(fig::FromValue));
651
652    let body = match &input.data {
653        Data::Struct(s) => from_value_struct(&s.fields, name, input)?,
654        Data::Enum(e) => from_value_enum(input, e)?,
655        Data::Union(_) => {
656            return Err(syn::Error::new_spanned(
657                input,
658                "fig's FromValue derive does not support unions",
659            ));
660        }
661    };
662
663    Ok(quote! {
664        impl #impl_g fig::FromValue for #name #ty_g #where_clause {
665            fn from_value(value: &fig::Value) -> ::core::result::Result<Self, fig::Error> {
666                #body
667            }
668        }
669    })
670}
671
672fn from_value_struct(
673    fields: &Fields,
674    name: &Ident,
675    input: &DeriveInput,
676) -> syn::Result<TokenStream2> {
677    match fields {
678        Fields::Named(named) => from_map_named(
679            named,
680            &quote! { Self },
681            &quote! { value },
682            &name.to_string(),
683            true,
684            container_rename_all(&input.attrs)?,
685        ),
686        Fields::Unnamed(unnamed) if unnamed.unnamed.len() == 1 => {
687            let ty = &unnamed.unnamed[0].ty;
688            Ok(quote! {
689                ::core::result::Result::Ok(Self(<#ty as fig::FromValue>::from_value(value)?))
690            })
691        }
692        Fields::Unnamed(_) => Err(syn::Error::new_spanned(
693            input,
694            "fig's FromValue derive supports newtype structs (one field) but not multi-field tuple structs yet",
695        )),
696        Fields::Unit => Ok(quote! { ::core::result::Result::Ok(Self) }),
697    }
698}
699
700/// Build `Result<Self, Error>` from a mapping value, for a named-field struct or
701/// struct variant. `ctor` is `Self` or `Self::Variant`; `map_value` is an
702/// expression evaluating to `&fig::Value`.
703fn from_map_named(
704    fields: &FieldsNamed,
705    ctor: &TokenStream2,
706    map_value: &TokenStream2,
707    type_label: &str,
708    allow_flatten: bool,
709    rename_all: Option<RenameRule>,
710) -> syn::Result<TokenStream2> {
711    let infos = collect_named_fields(fields, rename_all)?;
712    if !allow_flatten && let Some(f) = infos.iter().find(|f| f.flatten) {
713        return Err(syn::Error::new_spanned(
714            f.ident,
715            "`#[fig(flatten)]` is not supported inside enum variants yet",
716        ));
717    }
718
719    let known_keys: Vec<&String> = infos
720        .iter()
721        .filter(|f| !f.skip && !f.flatten)
722        .map(|f| &f.key)
723        .collect();
724    let has_flatten = infos.iter().any(|f| f.flatten && !f.skip);
725
726    let rest = if has_flatten {
727        quote! {
728            const __KNOWN: &[&str] = &[#(#known_keys),*];
729            let mut __rest: ::std::vec::Vec<(fig::Value, fig::Value)> = ::std::vec::Vec::new();
730            for (__k, __v) in __entries.iter() {
731                let __consumed = matches!(__k, fig::Value::Str(__s) if __KNOWN.contains(&__s.as_str()));
732                if !__consumed {
733                    __rest.push((__k.clone(), __v.clone()));
734                }
735            }
736            let __rest = fig::Value::Map(__rest);
737        }
738    } else {
739        quote! {}
740    };
741
742    let field_lets = infos.iter().map(|f| {
743        let ident = f.ident;
744        let ty = f.ty;
745        if f.skip {
746            return quote! { let #ident: #ty = ::core::default::Default::default(); };
747        }
748        if f.flatten {
749            return quote! {
750                let #ident: #ty = <#ty as fig::FromValue>::from_value(&__rest)?;
751            };
752        }
753        let key = &f.key;
754
755        // Common case — no alias, no custom `deserialize_with`, no custom
756        // default path — routes through the shared `fig::field`/`field_or_default`
757        // helpers. The lookup/convert/error scaffold is then compiled once per
758        // field *type* and shared, instead of inlined at every field site (the
759        // bulk of large derived `from_value` bodies, e.g. `Command`).
760        if f.aliases.is_empty() && f.deserialize_with.is_none() && f.default_path.is_none() {
761            if f.use_default {
762                return quote! { let #ident: #ty = fig::field_or_default(__entries, #key)?; };
763            }
764            return quote! { let #ident: #ty = fig::field(__entries, #key, #type_label)?; };
765        }
766
767        let missing = match &f.default_path {
768            Some(path) => quote! { #path() },
769            None if f.use_default => quote! { ::core::default::Default::default() },
770            None => {
771                quote! { return ::core::result::Result::Err(fig::Error::missing_field(#key, #type_label)) }
772            }
773        };
774        let present = match &f.deserialize_with {
775            Some(path) => quote! { #path(__v)? },
776            None => quote! { <#ty as fig::FromValue>::from_value(__v)? },
777        };
778        let aliases = &f.aliases;
779        quote! {
780            let #ident: #ty = match fig::map_get(__entries, #key)
781                #(.or_else(|| fig::map_get(__entries, #aliases)))*
782            {
783                ::std::option::Option::Some(__v) => #present,
784                ::std::option::Option::None => #missing,
785            };
786        }
787    });
788
789    let field_names = infos.iter().map(|f| f.ident);
790
791    Ok(quote! {{
792        let __entries = match #map_value {
793            fig::Value::Map(__e) => __e,
794            _ => return ::core::result::Result::Err(
795                fig::Error::expected_mapping(#type_label),
796            ),
797        };
798        let _ = &__entries;
799        #rest
800        #(#field_lets)*
801        ::core::result::Result::Ok(#ctor { #(#field_names),* })
802    }})
803}
804
805/// Build `Result<Self, Error>` for a non-unit variant from a `&fig::Value`
806/// expression `value_expr`, used wherever the variant's *content* is parsed
807/// (external map value, adjacent content, untagged whole value).
808fn build_variant(
809    variant: &Variant,
810    value_expr: &TokenStream2,
811    label: &str,
812) -> syn::Result<TokenStream2> {
813    let vident = &variant.ident;
814    match &variant.fields {
815        Fields::Unit => Ok(quote! { ::core::result::Result::Ok(Self::#vident) }),
816        Fields::Unnamed(u) if u.unnamed.len() == 1 => {
817            let ty = &u.unnamed[0].ty;
818            Ok(quote! {
819                ::core::result::Result::Ok(Self::#vident(<#ty as fig::FromValue>::from_value(#value_expr)?))
820            })
821        }
822        Fields::Unnamed(u) => {
823            let tys: Vec<&Type> = u.unnamed.iter().map(|f| &f.ty).collect();
824            let idxs: Vec<usize> = (0..tys.len()).collect();
825            let n = tys.len();
826            let seq_msg = format!("expected a sequence for tuple variant `{label}`");
827            Ok(quote! {{
828                let __items = match #value_expr {
829                    fig::Value::Seq(__s) => __s,
830                    _ => return ::core::result::Result::Err(
831                        fig::Error::msg_static(#seq_msg),
832                    ),
833                };
834                if __items.len() != #n {
835                    return ::core::result::Result::Err(
836                        fig::Error::wrong_seq_len(#label, #n, __items.len()),
837                    );
838                }
839                ::core::result::Result::Ok(Self::#vident(
840                    #(<#tys as fig::FromValue>::from_value(&__items[#idxs])?),*
841                ))
842            }})
843        }
844        Fields::Named(named) => from_map_named(
845            named,
846            &quote! { Self::#vident },
847            value_expr,
848            label,
849            false,
850            None,
851        ),
852    }
853}
854
855fn from_value_enum(input: &DeriveInput, data: &syn::DataEnum) -> syn::Result<TokenStream2> {
856    let tagging = tagging_of(input)?;
857    let rename_all = container_rename_all(&input.attrs)?;
858    let enum_name = input.ident.to_string();
859    match tagging {
860        Tagging::External => from_value_external(data, &enum_name, rename_all),
861        Tagging::Internal(tag) => from_value_internal(data, &enum_name, &tag, rename_all),
862        Tagging::Adjacent(tag, content) => {
863            from_value_adjacent(data, &enum_name, &tag, &content, rename_all)
864        }
865        Tagging::Untagged => from_value_untagged(data, &enum_name),
866    }
867}
868
869fn from_value_external(
870    data: &syn::DataEnum,
871    enum_name: &str,
872    rename_all: Option<RenameRule>,
873) -> syn::Result<TokenStream2> {
874    let mut unit_arms = Vec::new();
875    let mut map_arms = Vec::new();
876    for variant in &data.variants {
877        let vident = &variant.ident;
878        let key = variant_key(variant, rename_all)?;
879        let label = format!("{enum_name}::{vident}");
880        if matches!(variant.fields, Fields::Unit) {
881            unit_arms.push(quote! { #key => ::core::result::Result::Ok(Self::#vident), });
882        } else {
883            let body = build_variant(variant, &quote! { __v }, &label)?;
884            map_arms.push(quote! { #key => #body, });
885        }
886    }
887    let expected = format!("expected a string or single-key mapping for enum `{enum_name}`");
888    Ok(quote! {
889        match value {
890            fig::Value::Str(__s) => match __s.as_str() {
891                #(#unit_arms)*
892                __other => ::core::result::Result::Err(fig::Error::unknown_variant(#enum_name, __other)),
893            },
894            fig::Value::Map(__entries) if __entries.len() == 1 => {
895                let (__k, __v) = &__entries[0];
896                let __name = match __k {
897                    fig::Value::Str(__s) => __s.as_str(),
898                    _ => return ::core::result::Result::Err(
899                        fig::Error::msg_static("enum variant key must be a string"),
900                    ),
901                };
902                match __name {
903                    #(#map_arms)*
904                    __other => ::core::result::Result::Err(fig::Error::unknown_variant(#enum_name, __other)),
905                }
906            }
907            _ => ::core::result::Result::Err(fig::Error::msg_static(#expected)),
908        }
909    })
910}
911
912/// Shared prologue for internal/adjacent tagging: bind `__entries` (the mapping)
913/// and `__tag` (the tag string), or return an error.
914fn tag_prologue(enum_name: &str, tag: &str) -> TokenStream2 {
915    let not_map = format!("expected a mapping for tagged enum `{enum_name}`");
916    let missing_tag = format!("missing tag `{tag}` for enum `{enum_name}`");
917    let tag_kind = format!("tag `{tag}` for enum `{enum_name}` must be a string");
918    quote! {
919        let __entries = match value {
920            fig::Value::Map(__e) => __e,
921            _ => return ::core::result::Result::Err(
922                fig::Error::msg_static(#not_map),
923            ),
924        };
925        let __tag = match __entries.iter().rev().find_map(|(__k, __v)| match __k {
926            fig::Value::Str(__s) if __s == #tag => ::std::option::Option::Some(__v),
927            _ => ::std::option::Option::None,
928        }) {
929            ::std::option::Option::Some(fig::Value::Str(__s)) => __s.as_str(),
930            ::std::option::Option::Some(_) => return ::core::result::Result::Err(
931                fig::Error::msg_static(#tag_kind),
932            ),
933            ::std::option::Option::None => return ::core::result::Result::Err(
934                fig::Error::msg_static(#missing_tag),
935            ),
936        };
937    }
938}
939
940fn from_value_internal(
941    data: &syn::DataEnum,
942    enum_name: &str,
943    tag: &str,
944    rename_all: Option<RenameRule>,
945) -> syn::Result<TokenStream2> {
946    let mut arms = Vec::new();
947    for variant in &data.variants {
948        let vident = &variant.ident;
949        let key = variant_key(variant, rename_all)?;
950        let label = format!("{enum_name}::{vident}");
951        let arm = match &variant.fields {
952            Fields::Unit => quote! { #key => ::core::result::Result::Ok(Self::#vident), },
953            Fields::Named(named) => {
954                // Fields live in the same map as the tag; look them up directly.
955                let body = from_map_named(
956                    named,
957                    &quote! { Self::#vident },
958                    &quote! { value },
959                    &label,
960                    false,
961                    None,
962                )?;
963                quote! { #key => #body, }
964            }
965            Fields::Unnamed(u) if u.unnamed.len() == 1 => {
966                // Newtype: feed the inner type the map minus the tag entry.
967                let ty = &u.unnamed[0].ty;
968                quote! {
969                    #key => {
970                        let mut __rest: ::std::vec::Vec<(fig::Value, fig::Value)> = ::std::vec::Vec::new();
971                        for (__k, __v) in __entries.iter() {
972                            let __is_tag = matches!(__k, fig::Value::Str(__s) if __s == #tag);
973                            if !__is_tag {
974                                __rest.push((__k.clone(), __v.clone()));
975                            }
976                        }
977                        ::core::result::Result::Ok(Self::#vident(
978                            <#ty as fig::FromValue>::from_value(&fig::Value::Map(__rest))?,
979                        ))
980                    }
981                }
982            }
983            Fields::Unnamed(_) => {
984                return Err(syn::Error::new_spanned(
985                    variant,
986                    "internally tagged enums do not support tuple variants (matching serde)",
987                ));
988            }
989        };
990        arms.push(arm);
991    }
992    let prologue = tag_prologue(enum_name, tag);
993    Ok(quote! {
994        #prologue
995        match __tag {
996            #(#arms)*
997            __other => ::core::result::Result::Err(fig::Error::unknown_variant(#enum_name, __other)),
998        }
999    })
1000}
1001
1002fn from_value_adjacent(
1003    data: &syn::DataEnum,
1004    enum_name: &str,
1005    tag: &str,
1006    content: &str,
1007    rename_all: Option<RenameRule>,
1008) -> syn::Result<TokenStream2> {
1009    let mut arms = Vec::new();
1010    for variant in &data.variants {
1011        let vident = &variant.ident;
1012        let key = variant_key(variant, rename_all)?;
1013        let label = format!("{enum_name}::{vident}");
1014        if matches!(variant.fields, Fields::Unit) {
1015            arms.push(quote! { #key => ::core::result::Result::Ok(Self::#vident), });
1016        } else {
1017            let body = build_variant(variant, &quote! { __content_val }, &label)?;
1018            let missing = format!("missing content `{content}` for variant `{label}`");
1019            arms.push(quote! {
1020                #key => {
1021                    let __content_val = match __content {
1022                        ::std::option::Option::Some(__c) => __c,
1023                        ::std::option::Option::None => return ::core::result::Result::Err(
1024                            fig::Error::msg_static(#missing),
1025                        ),
1026                    };
1027                    #body
1028                }
1029            });
1030        }
1031    }
1032    let prologue = tag_prologue(enum_name, tag);
1033    Ok(quote! {
1034        #prologue
1035        let __content: ::std::option::Option<&fig::Value> =
1036            __entries.iter().rev().find_map(|(__k, __v)| match __k {
1037                fig::Value::Str(__s) if __s == #content => ::std::option::Option::Some(__v),
1038                _ => ::std::option::Option::None,
1039            });
1040        match __tag {
1041            #(#arms)*
1042            __other => ::core::result::Result::Err(fig::Error::unknown_variant(#enum_name, __other)),
1043        }
1044    })
1045}
1046
1047fn from_value_untagged(data: &syn::DataEnum, enum_name: &str) -> syn::Result<TokenStream2> {
1048    let mut attempts = Vec::new();
1049    for variant in &data.variants {
1050        let vident = &variant.ident;
1051        let label = format!("{enum_name}::{vident}");
1052        if matches!(variant.fields, Fields::Unit) {
1053            attempts.push(quote! {
1054                if matches!(value, fig::Value::Null) {
1055                    return ::core::result::Result::Ok(Self::#vident);
1056                }
1057            });
1058        } else {
1059            let body = build_variant(variant, &quote! { value }, &label)?;
1060            attempts.push(quote! {
1061                if let ::core::result::Result::Ok(__v) =
1062                    (|| -> ::core::result::Result<Self, fig::Error> { #body })()
1063                {
1064                    return ::core::result::Result::Ok(__v);
1065                }
1066            });
1067        }
1068    }
1069    let none = format!("no variant of enum `{enum_name}` matched the value");
1070    Ok(quote! {
1071        #(#attempts)*
1072        ::core::result::Result::Err(fig::Error::msg_static(#none))
1073    })
1074}