Skip to main content

gen_macros/
lib.rs

1//! `gen-macros` — proc-macros that auto-implement the universal
2//! `gen_types::ecosystem` traits.
3//!
4//! Materializes the Pillar 12 directive (generation over composition):
5//! a new package-manager adapter declares its typed shapes and the
6//! macros emit the `Spec` / `QuirkRegistry` impls. The author writes
7//! N lines of typed data; the trait surface is mechanical.
8//!
9//! See `theory/ECOSYSTEM-INTAKE.md` § "The macros" for the contract.
10//!
11//! ```ignore
12//! #[derive(SpecShape, serde::Serialize, serde::Deserialize)]
13//! #[spec(args = "BuildRustCrateArgs", quirk = "CrateQuirk")]
14//! pub struct BuildSpec {
15//!     pub version: u32,
16//!     pub crates: indexmap::IndexMap<String, CrateSpec>,
17//!     pub root_crate: String,
18//!     pub workspace_members: Vec<String>,
19//! }
20//! ```
21//!
22//! emits:
23//!
24//! ```ignore
25//! impl gen_types::Spec for BuildSpec {
26//!     type Args = BuildRustCrateArgs;
27//!     type Quirk = CrateQuirk;
28//!     fn schema_version(&self) -> u32 { self.version }
29//!     fn root_key(&self) -> &str { self.root_crate.as_str() }
30//!     fn member_keys(&self) -> Vec<&str> { self.workspace_members.iter().map(String::as_str).collect() }
31//!     fn args_for(&self, key: &str) -> Option<&Self::Args> {
32//!         self.crates.get(key).map(|c| &c.build_rust_crate_args)
33//!     }
34//!     fn quirks_for(&self, key: &str) -> &[Self::Quirk] {
35//!         self.crates.get(key).map(|c| c.quirks.as_slice()).unwrap_or(&[])
36//!     }
37//! }
38//! ```
39
40use proc_macro::TokenStream;
41use quote::quote;
42use syn::{parse_macro_input, Data, DeriveInput, Fields, Lit, Meta};
43
44/// `#[derive(SpecShape)]` — auto-implement `gen_types::Spec` on a
45/// struct whose fields follow the conventional gen build-spec shape:
46///
47/// - `version: u32`
48/// - `root_crate: String` (or `root_key: String`, opt-in via attr)
49/// - `workspace_members: Vec<String>`
50/// - `crates: IndexMap<String, T>` where `T` carries
51///   `build_rust_crate_args: Args` + `quirks: Vec<Quirk>`
52///
53/// Required attribute:
54/// `#[spec(args = "<ArgsTypeName>", quirk = "<QuirkTypeName>")]`
55///
56/// Optional attributes:
57/// `#[spec(args_field = "build_args")]` (default: `build_rust_crate_args`)
58/// `#[spec(root_field = "root_key")]`   (default: `root_crate`)
59/// `#[spec(members_field = "members")]` (default: `workspace_members`)
60/// `#[spec(crates_field = "packages")]` (default: `crates`)
61#[proc_macro_derive(SpecShape, attributes(spec))]
62pub fn derive_spec_shape(input: TokenStream) -> TokenStream {
63    let input = parse_macro_input!(input as DeriveInput);
64    let name = &input.ident;
65
66    let mut args_type: Option<String> = None;
67    let mut quirk_type: Option<String> = None;
68    let mut args_field = "build_rust_crate_args".to_string();
69    let mut root_field = "root_crate".to_string();
70    let mut members_field = "workspace_members".to_string();
71    let mut crates_field = "crates".to_string();
72
73    for attr in &input.attrs {
74        if !attr.path().is_ident("spec") {
75            continue;
76        }
77        let Meta::List(list) = &attr.meta else { continue };
78        let _ = list.parse_nested_meta(|meta| {
79            let Some(ident) = meta.path.get_ident() else {
80                return Ok(());
81            };
82            let value: Lit = meta.value()?.parse()?;
83            let Lit::Str(s) = value else {
84                return Ok(());
85            };
86            let v = s.value();
87            match ident.to_string().as_str() {
88                "args" => args_type = Some(v),
89                "quirk" => quirk_type = Some(v),
90                "args_field" => args_field = v,
91                "root_field" => root_field = v,
92                "members_field" => members_field = v,
93                "crates_field" => crates_field = v,
94                _ => {}
95            }
96            Ok(())
97        });
98    }
99
100    let args_type = match args_type {
101        Some(t) => syn::parse_str::<syn::Type>(&t).expect("invalid `args` type"),
102        None => {
103            return TokenStream::from(quote! {
104                compile_error!("SpecShape requires `#[spec(args = \"<TypeName>\", quirk = \"<TypeName>\")]`");
105            });
106        }
107    };
108    let quirk_type = match quirk_type {
109        Some(t) => syn::parse_str::<syn::Type>(&t).expect("invalid `quirk` type"),
110        None => {
111            return TokenStream::from(quote! {
112                compile_error!("SpecShape requires `#[spec(args = \"<TypeName>\", quirk = \"<TypeName>\")]`");
113            });
114        }
115    };
116
117    let args_field_ident = syn::Ident::new(&args_field, proc_macro2::Span::call_site());
118    let root_field_ident = syn::Ident::new(&root_field, proc_macro2::Span::call_site());
119    let members_field_ident = syn::Ident::new(&members_field, proc_macro2::Span::call_site());
120    let crates_field_ident = syn::Ident::new(&crates_field, proc_macro2::Span::call_site());
121
122    let expanded = quote! {
123        impl ::gen_types::Spec for #name {
124            type Args = #args_type;
125            type Quirk = #quirk_type;
126
127            fn schema_version(&self) -> u32 {
128                self.version
129            }
130
131            fn root_key(&self) -> &str {
132                self.#root_field_ident.as_str()
133            }
134
135            fn member_keys(&self) -> ::std::vec::Vec<&str> {
136                self.#members_field_ident.iter().map(::std::string::String::as_str).collect()
137            }
138
139            fn args_for(&self, key: &str) -> ::std::option::Option<&Self::Args> {
140                self.#crates_field_ident.get(key).map(|c| &c.#args_field_ident)
141            }
142
143            fn quirks_for(&self, key: &str) -> &[Self::Quirk] {
144                self.#crates_field_ident
145                    .get(key)
146                    .map(|c| c.quirks.as_slice())
147                    .unwrap_or(&[])
148            }
149        }
150    };
151
152    TokenStream::from(expanded)
153}
154
155/// `#[derive(QuirkRegistry)]` — auto-implement
156/// `gen_types::QuirkRegistry` on a marker struct that points at the
157/// real registry function.
158///
159/// Required attribute:
160/// `#[quirks(enum_name = "<QuirkEnumName>", registry_fn = "module::path::to::registry")]`
161///
162/// The `registry_fn` must be a `pub fn() -> Vec<(&'static str, Vec<Quirk>)>`
163/// the macro can call.
164#[proc_macro_derive(QuirkRegistry, attributes(quirks))]
165pub fn derive_quirk_registry(input: TokenStream) -> TokenStream {
166    let input = parse_macro_input!(input as DeriveInput);
167    let name = &input.ident;
168
169    let mut enum_name: Option<String> = None;
170    let mut registry_fn: Option<String> = None;
171    for attr in &input.attrs {
172        if !attr.path().is_ident("quirks") {
173            continue;
174        }
175        let Meta::List(list) = &attr.meta else { continue };
176        let _ = list.parse_nested_meta(|meta| {
177            let Some(ident) = meta.path.get_ident() else {
178                return Ok(());
179            };
180            let value: Lit = meta.value()?.parse()?;
181            let Lit::Str(s) = value else {
182                return Ok(());
183            };
184            let v = s.value();
185            match ident.to_string().as_str() {
186                "enum_name" => enum_name = Some(v),
187                "registry_fn" => registry_fn = Some(v),
188                _ => {}
189            }
190            Ok(())
191        });
192    }
193    let enum_ty = match enum_name {
194        Some(t) => syn::parse_str::<syn::Type>(&t).expect("invalid `enum_name`"),
195        None => {
196            return TokenStream::from(quote! {
197                compile_error!("QuirkRegistry requires `#[quirks(enum_name = \"<EnumName>\", registry_fn = \"<path>\")]`");
198            });
199        }
200    };
201    let reg_path = match registry_fn {
202        Some(t) => syn::parse_str::<syn::Path>(&t).expect("invalid `registry_fn`"),
203        None => {
204            return TokenStream::from(quote! {
205                compile_error!("QuirkRegistry requires `#[quirks(enum_name = \"<EnumName>\", registry_fn = \"<path>\")]`");
206            });
207        }
208    };
209
210    let expanded = quote! {
211        impl ::gen_types::QuirkRegistry for #name {
212            type Quirk = #enum_ty;
213
214            fn registry() -> ::std::vec::Vec<(&'static str, ::std::vec::Vec<Self::Quirk>)> {
215                #reg_path()
216            }
217        }
218    };
219
220    TokenStream::from(expanded)
221}
222
223/// `#[derive(TypedDispatcher)]` — auto-implement
224/// `gen_types::TypedDispatcher` on a Rust enum whose serde tag is
225/// `#[serde(tag = "kind", rename_all = "kebab-case")]`.
226///
227/// The macro observes the enum's variants and emits a trait impl
228/// reflecting the variant universe (kebab-case tags + per-variant
229/// field names). Substrate emitters consume the reflection to
230/// generate:
231///
232/// - the Nix `helpers = { ... }` table skeleton for the matching
233///   `substrate/lib/build/<eco>/quirk-apply.nix`;
234/// - the Lisp catalog entry naming the dispatcher;
235/// - a coverage test asserting every variant has a consumer arm.
236///
237/// Only unit variants and named-field struct variants are supported
238/// (the serde-tagged-enum shape pleme-io uses universally). Tuple
239/// variants raise a compile error.
240#[proc_macro_derive(TypedDispatcher)]
241pub fn derive_typed_dispatcher(input: TokenStream) -> TokenStream {
242    let input = parse_macro_input!(input as DeriveInput);
243    let name = &input.ident;
244
245    let Data::Enum(data) = &input.data else {
246        return TokenStream::from(quote! {
247            compile_error!("#[derive(TypedDispatcher)] only works on enums");
248        });
249    };
250
251    let mut kind_entries: Vec<proc_macro2::TokenStream> = Vec::new();
252    let mut field_entries: Vec<proc_macro2::TokenStream> = Vec::new();
253
254    for variant in &data.variants {
255        let tag = to_kebab_case(&variant.ident.to_string());
256        let fields = match &variant.fields {
257            Fields::Named(named) => named
258                .named
259                .iter()
260                .filter_map(|f| f.ident.as_ref().map(std::string::ToString::to_string))
261                .collect::<Vec<_>>(),
262            Fields::Unit => Vec::new(),
263            Fields::Unnamed(_) => {
264                let msg = format!(
265                    "#[derive(TypedDispatcher)] variant `{}` uses tuple fields; only named-field and unit variants are supported (matches the serde-tagged-enum shape pleme-io requires)",
266                    variant.ident
267                );
268                return TokenStream::from(quote! {
269                    compile_error!(#msg);
270                });
271            }
272        };
273
274        kind_entries.push(quote! { #tag });
275        let field_strs: Vec<proc_macro2::TokenStream> =
276            fields.iter().map(|f| quote! { #f }).collect();
277        field_entries.push(quote! {
278            (#tag, ::std::vec![ #( #field_strs ),* ])
279        });
280    }
281
282    let expanded = quote! {
283        impl ::gen_types::TypedDispatcher for #name {
284            fn variant_kinds() -> ::std::vec::Vec<&'static str> {
285                ::std::vec![ #( #kind_entries ),* ]
286            }
287
288            fn variant_fields() -> ::std::vec::Vec<(&'static str, ::std::vec::Vec<&'static str>)> {
289                ::std::vec![ #( #field_entries ),* ]
290            }
291        }
292    };
293
294    TokenStream::from(expanded)
295}
296
297// ── Discriminant + IsVariant — typed-reflection derive surface ───
298//
299// These two derives are the substrate-wide derive surface for typed
300// enums (PATTERN-EXTRACTION.md Patterns 6 + sibling). They live in
301// gen-macros (next to TypedDispatcher) so consumers in any pleme-io
302// crate that already depends on gen-platform can reach for them
303// without adding a fresh derive crate.
304//
305// Discriminant emits `pub const fn <method>(&self) -> &'static str`
306// returning the variant name as a stable lowercase / kebab-case /
307// snake_case / title-case identifier.
308//
309// IsVariant emits `pub const fn is_<variant>(&self) -> bool`
310// per variant.
311//
312// Both support per-variant `#[discriminant(name = "...")]` /
313// `#[is_variant(name = "...")]` overrides for cases where the
314// auto-derived name doesn't match the historical wire format.
315
316#[derive(Clone, Copy)]
317enum DiscriminantCase {
318    Kebab,
319    Snake,
320    Lower,
321    Title,
322}
323
324impl DiscriminantCase {
325    fn apply(self, s: &str) -> String {
326        match self {
327            DiscriminantCase::Kebab => to_kebab_case(s),
328            DiscriminantCase::Snake => discriminant_to_snake(s),
329            DiscriminantCase::Lower => s.to_ascii_lowercase(),
330            DiscriminantCase::Title => s.to_string(),
331        }
332    }
333
334    fn parse(s: &str) -> Option<Self> {
335        match s {
336            "kebab" | "kebab-case" => Some(DiscriminantCase::Kebab),
337            "snake" | "snake_case" => Some(DiscriminantCase::Snake),
338            "lower" | "lowercase" => Some(DiscriminantCase::Lower),
339            "title" | "Title" | "TitleCase" => Some(DiscriminantCase::Title),
340            _ => None,
341        }
342    }
343}
344
345fn discriminant_to_snake(s: &str) -> String {
346    let mut out = String::with_capacity(s.len() + 4);
347    for (i, c) in s.chars().enumerate() {
348        if c.is_ascii_uppercase() {
349            if i > 0 {
350                out.push('_');
351            }
352            out.push(c.to_ascii_lowercase());
353        } else {
354            out.push(c);
355        }
356    }
357    out
358}
359
360fn discriminant_variant_pattern(v: &syn::Variant) -> proc_macro2::TokenStream {
361    let name = &v.ident;
362    match &v.fields {
363        Fields::Unit => quote! { Self::#name },
364        Fields::Unnamed(_) => quote! { Self::#name(..) },
365        Fields::Named(_) => quote! { Self::#name { .. } },
366    }
367}
368
369fn discriminant_variant_explicit_name(v: &syn::Variant) -> Option<String> {
370    for attr in &v.attrs {
371        if !attr.path().is_ident("discriminant") {
372            continue;
373        }
374        let mut out = None;
375        let _ = attr.parse_nested_meta(|meta| {
376            if meta.path.is_ident("name") {
377                let value = meta.value()?;
378                let s: syn::LitStr = value.parse()?;
379                out = Some(s.value());
380            }
381            Ok(())
382        });
383        if out.is_some() {
384            return out;
385        }
386    }
387    None
388}
389
390/// `#[derive(Discriminant)]` — auto-implement
391/// `pub const fn <method>(&self) -> &'static str` returning the
392/// variant name as a stable case-folded identifier.
393///
394/// # Attributes
395///
396/// - `#[discriminant(method = "kind")]` — method name (default
397///   `"discriminant"`)
398/// - `#[discriminant(case = "kebab" | "snake" | "lower" | "title")]`
399///   — variant-name case transformation (default `"kebab"`)
400/// - `#[discriminant(also_display)]` — also emit `impl Display`
401///   delegating to the method (writes the variant string to the
402///   formatter). Eliminates the boilerplate Display impl that
403///   recurs across the substrate for typed enums where Display
404///   IS the discriminant.
405/// - Per-variant `#[discriminant(name = "explicit-name")]` overrides
406///   the auto-derived name (used when the wire format pre-dates the
407///   rule).
408///
409/// Compounding: pairs naturally with `#[derive(IsVariant)]` (predicate
410/// methods) and `#[derive(TypedDispatcher)]` (variant → consumer arm
411/// dispatch). All three target the same closed-variant-universe shape
412/// the pleme-io substrate uses everywhere.
413#[proc_macro_derive(Discriminant, attributes(discriminant))]
414pub fn derive_discriminant(input: TokenStream) -> TokenStream {
415    let input = parse_macro_input!(input as DeriveInput);
416    let enum_name = input.ident.clone();
417
418    let Data::Enum(de) = input.data.clone() else {
419        return syn::Error::new_spanned(
420            &enum_name,
421            "#[derive(Discriminant)] is only valid on enums",
422        )
423        .to_compile_error()
424        .into();
425    };
426
427    let mut method = "discriminant".to_string();
428    let mut case = DiscriminantCase::Kebab;
429    let mut also_display = false;
430    for attr in &input.attrs {
431        if !attr.path().is_ident("discriminant") {
432            continue;
433        }
434        let _ = attr.parse_nested_meta(|meta| {
435            if meta.path.is_ident("method") {
436                let value = meta.value()?;
437                let s: syn::LitStr = value.parse()?;
438                method = s.value();
439            } else if meta.path.is_ident("case") {
440                let value = meta.value()?;
441                let s: syn::LitStr = value.parse()?;
442                if let Some(c) = DiscriminantCase::parse(&s.value()) {
443                    case = c;
444                }
445            } else if meta.path.is_ident("also_display") {
446                also_display = true;
447            }
448            Ok(())
449        });
450    }
451    let method_ident = syn::Ident::new(&method, proc_macro2::Span::call_site());
452
453    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
454
455    let arms: Vec<proc_macro2::TokenStream> = de
456        .variants
457        .iter()
458        .map(|v| {
459            let pattern = discriminant_variant_pattern(v);
460            let name_str = discriminant_variant_explicit_name(v)
461                .unwrap_or_else(|| case.apply(&v.ident.to_string()));
462            quote! { #pattern => #name_str }
463        })
464        .collect();
465
466    let display_impl = if also_display {
467        quote! {
468            impl #impl_generics ::core::fmt::Display for #enum_name #ty_generics #where_clause {
469                fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
470                    f.write_str(self.#method_ident())
471                }
472            }
473        }
474    } else {
475        quote! {}
476    };
477
478    let expanded = quote! {
479        impl #impl_generics #enum_name #ty_generics #where_clause {
480            /// Stable variant discriminant — auto-generated by
481            /// `#[derive(Discriminant)]`. The string IS the wire
482            /// identifier for metrics labels / audit-log tags /
483            /// rate-limit keys; renaming an existing variant is a
484            /// breaking change.
485            pub const fn #method_ident(&self) -> &'static str {
486                match self {
487                    #(#arms),*
488                }
489            }
490        }
491        #display_impl
492    };
493
494    expanded.into()
495}
496
497/// `#[derive(FromStrKind)]` — the inverse of Discriminant. Parses
498/// a string back to a variant using the same case-folded variant
499/// name. Only unit variants are supported (data variants need
500/// caller-supplied payloads — out of scope for a string-only parse).
501///
502/// # Attributes
503///
504/// - `#[from_str_kind(case = "kebab" | "snake" | "lower" | "title")]`
505///   — case transform matching the wire format (default `"kebab"`)
506/// - Per-variant `#[from_str_kind(name = "explicit")]` — match a
507///   specific wire string for this variant (overrides case transform)
508/// - `#[from_str_kind(error = "MyEnumParseError")]` — name of the
509///   generated error type (default `<EnumName>ParseError`)
510///
511/// Pairs with Discriminant: when both derives are on the same enum
512/// with the same case transform, `s.parse() -> Ok(v); v.discriminant() == s`
513/// — a typed round-trip.
514#[proc_macro_derive(FromStrKind, attributes(from_str_kind))]
515pub fn derive_from_str_kind(input: TokenStream) -> TokenStream {
516    let input = parse_macro_input!(input as DeriveInput);
517    let enum_name = input.ident.clone();
518
519    let Data::Enum(de) = input.data.clone() else {
520        return syn::Error::new_spanned(
521            &enum_name,
522            "#[derive(FromStrKind)] is only valid on enums",
523        )
524        .to_compile_error()
525        .into();
526    };
527
528    let mut case = DiscriminantCase::Kebab;
529    let mut error_name = format!("{enum_name}ParseError");
530    for attr in &input.attrs {
531        if !attr.path().is_ident("from_str_kind") {
532            continue;
533        }
534        let _ = attr.parse_nested_meta(|meta| {
535            if meta.path.is_ident("case") {
536                let value = meta.value()?;
537                let s: syn::LitStr = value.parse()?;
538                if let Some(c) = DiscriminantCase::parse(&s.value()) {
539                    case = c;
540                }
541            } else if meta.path.is_ident("error") {
542                let value = meta.value()?;
543                let s: syn::LitStr = value.parse()?;
544                error_name = s.value();
545            }
546            Ok(())
547        });
548    }
549    let error_ident = syn::Ident::new(&error_name, proc_macro2::Span::call_site());
550
551    let mut arms: Vec<proc_macro2::TokenStream> = Vec::new();
552    let mut known_strings: Vec<String> = Vec::new();
553    for v in &de.variants {
554        if !matches!(v.fields, Fields::Unit) {
555            return syn::Error::new_spanned(
556                &v.ident,
557                "#[derive(FromStrKind)] requires all variants to be unit variants (no data payloads)",
558            )
559            .to_compile_error()
560            .into();
561        }
562        let v_ident = &v.ident;
563        let explicit = v.attrs.iter().find_map(|attr| {
564            if !attr.path().is_ident("from_str_kind") {
565                return None;
566            }
567            let mut out = None;
568            let _ = attr.parse_nested_meta(|meta| {
569                if meta.path.is_ident("name") {
570                    let value = meta.value()?;
571                    let s: syn::LitStr = value.parse()?;
572                    out = Some(s.value());
573                }
574                Ok(())
575            });
576            out
577        });
578        let name_str = explicit.unwrap_or_else(|| case.apply(&v_ident.to_string()));
579        known_strings.push(name_str.clone());
580        arms.push(quote! { #name_str => Ok(Self::#v_ident) });
581    }
582
583    let known_list = known_strings.join(" | ");
584    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
585
586    let expanded = quote! {
587        /// Auto-generated parse error for the matching `FromStrKind` impl.
588        #[derive(Debug, Clone, PartialEq, Eq)]
589        pub struct #error_ident {
590            pub input: ::std::string::String,
591        }
592
593        impl ::core::fmt::Display for #error_ident {
594            fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
595                write!(
596                    f,
597                    "unknown variant {input:?}; expected one of: {known}",
598                    input = self.input,
599                    known = #known_list,
600                )
601            }
602        }
603
604        impl ::std::error::Error for #error_ident {}
605
606        impl #impl_generics ::core::str::FromStr for #enum_name #ty_generics #where_clause {
607            type Err = #error_ident;
608            fn from_str(s: &str) -> ::core::result::Result<Self, Self::Err> {
609                match s {
610                    #(#arms),*,
611                    other => Err(#error_ident { input: other.to_string() }),
612                }
613            }
614        }
615    };
616
617    expanded.into()
618}
619
620fn is_variant_method_name(v: &syn::Variant) -> syn::Ident {
621    let explicit = v.attrs.iter().find_map(|attr| {
622        if !attr.path().is_ident("is_variant") {
623            return None;
624        }
625        let mut out = None;
626        let _ = attr.parse_nested_meta(|meta| {
627            if meta.path.is_ident("name") {
628                let value = meta.value()?;
629                let s: syn::LitStr = value.parse()?;
630                out = Some(s.value());
631            }
632            Ok(())
633        });
634        out
635    });
636    let snake = explicit.unwrap_or_else(|| discriminant_to_snake(&v.ident.to_string()));
637    syn::Ident::new(&format!("is_{snake}"), proc_macro2::Span::call_site())
638}
639
640/// `#[derive(IsVariant)]` — auto-implement `pub const fn is_<variant>(&self) -> bool`
641/// for every variant.
642///
643/// # Attributes
644///
645/// - Per-variant `#[is_variant(name = "explicit")]` overrides the
646///   auto-derived method-name suffix (default is the snake-cased
647///   variant identifier).
648///
649/// Compounding: pairs with `#[derive(Discriminant)]` for variant→name
650/// reflection and `#[derive(TypedDispatcher)]` for variant→consumer
651/// dispatch.
652#[proc_macro_derive(IsVariant, attributes(is_variant))]
653pub fn derive_is_variant(input: TokenStream) -> TokenStream {
654    let input = parse_macro_input!(input as DeriveInput);
655    let enum_name = input.ident.clone();
656
657    let Data::Enum(de) = input.data.clone() else {
658        return syn::Error::new_spanned(
659            &enum_name,
660            "#[derive(IsVariant)] is only valid on enums",
661        )
662        .to_compile_error()
663        .into();
664    };
665
666    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
667
668    let methods: Vec<proc_macro2::TokenStream> = de
669        .variants
670        .iter()
671        .map(|v| {
672            let pattern = discriminant_variant_pattern(v);
673            let method_name = is_variant_method_name(v);
674            quote! {
675                pub const fn #method_name(&self) -> bool {
676                    matches!(self, #pattern)
677                }
678            }
679        })
680        .collect();
681
682    let expanded = quote! {
683        impl #impl_generics #enum_name #ty_generics #where_clause {
684            #(#methods)*
685        }
686    };
687
688    expanded.into()
689}
690
691/// `#[derive(BackendError)]` — auto-implement a trait with the
692/// shape:
693///
694/// ```ignore
695/// pub trait BackendError {
696///     fn is_retryable(&self) -> bool;
697///     fn is_auth_failure(&self) -> bool { false }
698///     fn kind(&self) -> &'static str;
699/// }
700/// ```
701///
702/// The derive emits:
703///   - `is_retryable`  — `true` for variants tagged `#[backend_error(transient)]`
704///   - `is_auth_failure` — `true` for variants tagged `#[backend_error(auth)]`
705///   - `kind` — delegates to `self.discriminant()` (requires
706///     `#[derive(Discriminant)]` to be on the same enum with method
707///     `discriminant` OR the consumer overrides via `kind_method`)
708///
709/// # Attributes
710///
711/// - `#[backend_error(trait_path = "::path::to::BackendError")]` —
712///   fully-qualified trait path. Defaults to unqualified
713///   `BackendError` — consumer must `use the_trait::BackendError` in
714///   scope.
715/// - `#[backend_error(kind_method = "kind")]` — name of the
716///   `&'static str`-returning method to delegate `kind()` to (default
717///   `"discriminant"` — matches the default Discriminant method name).
718/// - Per-variant `#[backend_error(transient)]` — variant is transient
719///   (caller should retry).
720/// - Per-variant `#[backend_error(auth)]` — variant is an auth
721///   failure (HTTP 401/403 maps).
722/// - Per-variant `#[backend_error(permanent)]` — variant is permanent
723///   (caller must not retry). Default for unattributed variants.
724///
725/// # Round-trip with Discriminant
726///
727/// Pairs with Discriminant to deliver the BackendError contract in
728/// two derives:
729///
730/// ```ignore
731/// use magma_converge::BackendError;  // trait in scope
732///
733/// #[derive(Debug, thiserror::Error, gen_platform::Discriminant, gen_platform::BackendError)]
734/// #[discriminant(method = "discriminant", case = "snake")]
735/// enum BlobStoreError {
736///     #[error("not found at {path:?}")]
737///     NotFound { path: String },
738///
739///     #[error("permission denied at {path:?}")]
740///     #[backend_error(auth)]
741///     PermissionDenied { path: String },
742///
743///     #[error("transient at {path:?}")]
744///     #[backend_error(transient)]
745///     Transient { path: String },
746///
747///     #[error("permanent at {path:?}")]
748///     Permanent { path: String },
749/// }
750///
751/// // Auto-generated:
752/// //   impl BackendError for BlobStoreError {
753/// //       fn is_retryable(&self) -> bool { matches!(self, Self::Transient { .. }) }
754/// //       fn is_auth_failure(&self) -> bool { matches!(self, Self::PermissionDenied { .. }) }
755/// //       fn kind(&self) -> &'static str { self.discriminant() }
756/// //   }
757/// ```
758#[proc_macro_derive(BackendError, attributes(backend_error))]
759pub fn derive_backend_error(input: TokenStream) -> TokenStream {
760    let input = parse_macro_input!(input as DeriveInput);
761    let enum_name = input.ident.clone();
762
763    let Data::Enum(de) = input.data.clone() else {
764        return syn::Error::new_spanned(
765            &enum_name,
766            "#[derive(BackendError)] is only valid on enums",
767        )
768        .to_compile_error()
769        .into();
770    };
771
772    let mut trait_path: syn::Path = syn::parse_quote!(BackendError);
773    let mut kind_method = "discriminant".to_string();
774    for attr in &input.attrs {
775        if !attr.path().is_ident("backend_error") {
776            continue;
777        }
778        let _ = attr.parse_nested_meta(|meta| {
779            if meta.path.is_ident("trait_path") {
780                let value = meta.value()?;
781                let s: syn::LitStr = value.parse()?;
782                if let Ok(p) = syn::parse_str::<syn::Path>(&s.value()) {
783                    trait_path = p;
784                }
785            } else if meta.path.is_ident("kind_method") {
786                let value = meta.value()?;
787                let s: syn::LitStr = value.parse()?;
788                kind_method = s.value();
789            }
790            Ok(())
791        });
792    }
793    let kind_method_ident = syn::Ident::new(&kind_method, proc_macro2::Span::call_site());
794
795    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
796
797    let mut transient_patterns: Vec<proc_macro2::TokenStream> = Vec::new();
798    let mut auth_patterns: Vec<proc_macro2::TokenStream> = Vec::new();
799    for v in &de.variants {
800        let mut tags: std::collections::HashSet<String> = std::collections::HashSet::new();
801        for attr in &v.attrs {
802            if !attr.path().is_ident("backend_error") {
803                continue;
804            }
805            let _ = attr.parse_nested_meta(|meta| {
806                if let Some(ident) = meta.path.get_ident() {
807                    tags.insert(ident.to_string());
808                }
809                Ok(())
810            });
811        }
812        let pattern = discriminant_variant_pattern(v);
813        if tags.contains("transient") {
814            transient_patterns.push(pattern.clone());
815        }
816        if tags.contains("auth") {
817            auth_patterns.push(pattern.clone());
818        }
819    }
820
821    let is_retryable_body = if transient_patterns.is_empty() {
822        quote! { false }
823    } else {
824        quote! { matches!(self, #(#transient_patterns)|*) }
825    };
826    let is_auth_failure_body = if auth_patterns.is_empty() {
827        quote! { false }
828    } else {
829        quote! { matches!(self, #(#auth_patterns)|*) }
830    };
831
832    let expanded = quote! {
833        impl #impl_generics #trait_path for #enum_name #ty_generics #where_clause {
834            fn is_retryable(&self) -> bool {
835                #is_retryable_body
836            }
837
838            fn is_auth_failure(&self) -> bool {
839                #is_auth_failure_body
840            }
841
842            fn kind(&self) -> &'static str {
843                self.#kind_method_ident()
844            }
845        }
846    };
847
848    expanded.into()
849}
850
851/// `#[derive(OutcomeLattice)]` — auto-implement an OutcomeLattice
852/// trait from per-variant severity attributes.
853///
854/// Expected trait shape:
855///
856/// ```ignore
857/// pub trait OutcomeLattice: Clone + PartialEq {
858///     fn severity(&self) -> u32;
859///     fn baseline() -> Self;
860///     fn worst(&self, other: &Self) -> Self { ... }
861///     fn best(&self, other: &Self) -> Self { ... }
862/// }
863/// ```
864///
865/// The derive emits `severity()` (from per-variant attrs) +
866/// `baseline()` (returning the single `#[outcome(baseline)]`-tagged
867/// unit variant). `worst` + `best` come from the trait's defaults.
868///
869/// # Attributes
870///
871/// - `#[outcome_lattice(trait_path = "::path::to::OutcomeLattice")]`
872///   — fully-qualified trait path (default unqualified
873///   `OutcomeLattice` — consumer must `use the_trait::OutcomeLattice`
874///   in scope).
875/// - Per-variant `#[outcome(severity = N)]` — severity for this
876///   variant (u32; default 0 if unspecified).
877/// - Per-variant `#[outcome(baseline)]` — marks the unit variant
878///   that `baseline()` returns. Exactly ONE variant must carry this;
879///   it must be a unit variant.
880///
881/// # Example
882///
883/// ```ignore
884/// use magma_converge::outcome::OutcomeLattice;
885///
886/// #[derive(Clone, PartialEq, gen_platform::OutcomeLattice)]
887/// enum ReadyState {
888///     #[outcome(severity = 0, baseline)]
889///     Ready,
890///     #[outcome(severity = 1)]
891///     Unknown,
892///     #[outcome(severity = 2)]
893///     InProgress { reason: String },
894///     #[outcome(severity = 3)]
895///     Failed { reason: String },
896/// }
897///
898/// // Auto-generated:
899/// //   impl OutcomeLattice for ReadyState {
900/// //       fn severity(&self) -> u32 { match self { ... } }
901/// //       fn baseline() -> Self { Self::Ready }
902/// //   }
903/// ```
904#[proc_macro_derive(OutcomeLattice, attributes(outcome_lattice, outcome))]
905pub fn derive_outcome_lattice(input: TokenStream) -> TokenStream {
906    let input = parse_macro_input!(input as DeriveInput);
907    let enum_name = input.ident.clone();
908
909    let Data::Enum(de) = input.data.clone() else {
910        return syn::Error::new_spanned(
911            &enum_name,
912            "#[derive(OutcomeLattice)] is only valid on enums",
913        )
914        .to_compile_error()
915        .into();
916    };
917
918    let mut trait_path: syn::Path = syn::parse_quote!(OutcomeLattice);
919    for attr in &input.attrs {
920        if !attr.path().is_ident("outcome_lattice") {
921            continue;
922        }
923        let _ = attr.parse_nested_meta(|meta| {
924            if meta.path.is_ident("trait_path") {
925                let value = meta.value()?;
926                let s: syn::LitStr = value.parse()?;
927                if let Ok(p) = syn::parse_str::<syn::Path>(&s.value()) {
928                    trait_path = p;
929                }
930            }
931            Ok(())
932        });
933    }
934
935    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
936
937    let mut severity_arms: Vec<proc_macro2::TokenStream> = Vec::new();
938    let mut baseline_variant: Option<syn::Ident> = None;
939    for v in &de.variants {
940        let v_ident = &v.ident;
941        let pattern = discriminant_variant_pattern(v);
942        let mut sev: u32 = 0;
943        let mut is_baseline = false;
944        for attr in &v.attrs {
945            if !attr.path().is_ident("outcome") {
946                continue;
947            }
948            let _ = attr.parse_nested_meta(|meta| {
949                if meta.path.is_ident("severity") {
950                    let value = meta.value()?;
951                    let lit: syn::LitInt = value.parse()?;
952                    sev = lit.base10_parse::<u32>().unwrap_or(0);
953                } else if meta.path.is_ident("baseline") {
954                    is_baseline = true;
955                }
956                Ok(())
957            });
958        }
959        if is_baseline {
960            if !matches!(v.fields, Fields::Unit) {
961                return syn::Error::new_spanned(
962                    v_ident,
963                    "#[outcome(baseline)] requires a unit variant",
964                )
965                .to_compile_error()
966                .into();
967            }
968            if baseline_variant.is_some() {
969                return syn::Error::new_spanned(
970                    v_ident,
971                    "exactly one variant may carry #[outcome(baseline)]",
972                )
973                .to_compile_error()
974                .into();
975            }
976            baseline_variant = Some(v_ident.clone());
977        }
978        let lit = syn::LitInt::new(&sev.to_string(), proc_macro2::Span::call_site());
979        severity_arms.push(quote! { #pattern => #lit });
980    }
981
982    let Some(baseline_ident) = baseline_variant else {
983        return syn::Error::new_spanned(
984            &enum_name,
985            "exactly one variant must carry #[outcome(baseline)] to derive OutcomeLattice",
986        )
987        .to_compile_error()
988        .into();
989    };
990
991    let expanded = quote! {
992        impl #impl_generics #trait_path for #enum_name #ty_generics #where_clause {
993            fn severity(&self) -> u32 {
994                match self {
995                    #(#severity_arms),*
996                }
997            }
998
999            fn baseline() -> Self {
1000                Self::#baseline_ident
1001            }
1002        }
1003    };
1004
1005    expanded.into()
1006}
1007
1008/// Convert PascalCase variant identifiers to kebab-case serde tags.
1009/// Mirrors `#[serde(rename_all = "kebab-case")]` semantics via
1010/// heck-style word boundaries: lower→upper and digit→upper both
1011/// trigger a hyphen. `Wasm32Wasi` → `wasm32-wasi`.
1012fn to_kebab_case(s: &str) -> String {
1013    let mut out = String::with_capacity(s.len() + 4);
1014    let mut prev_lower = false;
1015    let mut prev_digit = false;
1016    for ch in s.chars() {
1017        if ch.is_ascii_uppercase() {
1018            if prev_lower || prev_digit {
1019                out.push('-');
1020            }
1021            for c in ch.to_lowercase() {
1022                out.push(c);
1023            }
1024            prev_lower = false;
1025            prev_digit = false;
1026        } else if ch.is_ascii_digit() {
1027            out.push(ch);
1028            prev_lower = false;
1029            prev_digit = true;
1030        } else {
1031            out.push(ch);
1032            prev_lower = true;
1033            prev_digit = false;
1034        }
1035    }
1036    out
1037}
1038
1039// ── #[fsm(label = "...")] attribute macro ──────────────────────────
1040//
1041// Bundles the canonical typed-FSM-enum boilerplate that every typed
1042// dispatcher class across pleme-io ships:
1043//
1044//   #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1045//   #[derive(TypedDispatcher, Discriminant, IsVariant)]
1046//   #[serde(tag = "kind", rename_all = "kebab-case")]
1047//   pub enum Foo { ... }
1048//   gen_platform::register_dispatcher!("eco.foo", Foo);
1049//
1050// becomes:
1051//
1052//   #[gen_macros::fsm(label = "eco.foo")]
1053//   pub enum Foo { ... }
1054//
1055// Roughly 8 lines → 1 line per typed-FSM, fleet-wide. Same shape,
1056// no manual catalog wiring drift.
1057
1058/// Attribute macro that bundles the typed-FSM derive quintet +
1059/// serde tag + catalog registration. Use on a closed enum:
1060///
1061/// ```ignore
1062/// #[gen_macros::fsm(label = "gen.cargo.lock-lifecycle-state")]
1063/// pub enum LockLifecycleState {
1064///     Unlocked { current_lock_hash: String },
1065///     Locked   { spec_hash: String, lock_hash: String },
1066///     Drifted  { committed_lock_hash: String, current_lock_hash: String },
1067///     MissingLock,
1068/// }
1069/// ```
1070///
1071/// Expands to:
1072///
1073/// ```ignore
1074/// #[derive(Clone, Debug, PartialEq, Eq,
1075///          serde::Serialize, serde::Deserialize,
1076///          gen_macros::TypedDispatcher,
1077///          gen_macros::Discriminant,
1078///          gen_macros::IsVariant)]
1079/// #[serde(tag = "kind", rename_all = "kebab-case")]
1080/// pub enum LockLifecycleState { /* ... */ }
1081///
1082/// gen_platform::register_dispatcher!(
1083///     "gen.cargo.lock-lifecycle-state",
1084///     LockLifecycleState
1085/// );
1086/// ```
1087///
1088/// The consumer crate must depend on `gen_macros`, `gen_platform`,
1089/// and `serde` with the `derive` feature.
1090#[proc_macro_attribute]
1091pub fn fsm(args: TokenStream, item: TokenStream) -> TokenStream {
1092    let args = parse_macro_input!(args as FsmArgs);
1093    let item = parse_macro_input!(item as syn::ItemEnum);
1094    let name = &item.ident;
1095    let label = &args.label;
1096
1097    quote! {
1098        #[derive(
1099            ::core::clone::Clone,
1100            ::core::fmt::Debug,
1101            ::core::cmp::PartialEq,
1102            ::core::cmp::Eq,
1103            ::serde::Serialize,
1104            ::serde::Deserialize,
1105            ::gen_macros::TypedDispatcher,
1106            ::gen_macros::Discriminant,
1107            ::gen_macros::IsVariant,
1108        )]
1109        #[serde(tag = "kind", rename_all = "kebab-case")]
1110        #item
1111
1112        ::gen_platform::register_dispatcher!(#label, #name);
1113    }
1114    .into()
1115}
1116
1117struct FsmArgs {
1118    label: String,
1119}
1120
1121impl syn::parse::Parse for FsmArgs {
1122    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
1123        let kw: syn::Ident = input.parse()?;
1124        if kw != "label" {
1125            return Err(syn::Error::new(
1126                kw.span(),
1127                "expected `label = \"<catalog-label>\"`",
1128            ));
1129        }
1130        input.parse::<syn::Token![=]>()?;
1131        let lit: syn::LitStr = input.parse()?;
1132        Ok(Self { label: lit.value() })
1133    }
1134}