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