1use proc_macro::TokenStream;
41use quote::quote;
42use syn::{Data, DeriveInput, Fields, Lit, Meta, parse_macro_input};
43
44#[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 {
78 continue;
79 };
80 let _ = list.parse_nested_meta(|meta| {
81 let Some(ident) = meta.path.get_ident() else {
82 return Ok(());
83 };
84 let value: Lit = meta.value()?.parse()?;
85 let Lit::Str(s) = value else {
86 return Ok(());
87 };
88 let v = s.value();
89 match ident.to_string().as_str() {
90 "args" => args_type = Some(v),
91 "quirk" => quirk_type = Some(v),
92 "args_field" => args_field = v,
93 "root_field" => root_field = v,
94 "members_field" => members_field = v,
95 "crates_field" => crates_field = v,
96 _ => {}
97 }
98 Ok(())
99 });
100 }
101
102 let args_type = match args_type {
103 Some(t) => syn::parse_str::<syn::Type>(&t).expect("invalid `args` type"),
104 None => {
105 return TokenStream::from(quote! {
106 compile_error!("SpecShape requires `#[spec(args = \"<TypeName>\", quirk = \"<TypeName>\")]`");
107 });
108 }
109 };
110 let quirk_type = match quirk_type {
111 Some(t) => syn::parse_str::<syn::Type>(&t).expect("invalid `quirk` type"),
112 None => {
113 return TokenStream::from(quote! {
114 compile_error!("SpecShape requires `#[spec(args = \"<TypeName>\", quirk = \"<TypeName>\")]`");
115 });
116 }
117 };
118
119 let args_field_ident = syn::Ident::new(&args_field, proc_macro2::Span::call_site());
120 let root_field_ident = syn::Ident::new(&root_field, proc_macro2::Span::call_site());
121 let members_field_ident = syn::Ident::new(&members_field, proc_macro2::Span::call_site());
122 let crates_field_ident = syn::Ident::new(&crates_field, proc_macro2::Span::call_site());
123
124 let expanded = quote! {
125 impl ::gen_types::Spec for #name {
126 type Args = #args_type;
127 type Quirk = #quirk_type;
128
129 fn schema_version(&self) -> u32 {
130 self.version
131 }
132
133 fn root_key(&self) -> &str {
134 self.#root_field_ident.as_str()
135 }
136
137 fn member_keys(&self) -> ::std::vec::Vec<&str> {
138 self.#members_field_ident.iter().map(::std::string::String::as_str).collect()
139 }
140
141 fn args_for(&self, key: &str) -> ::std::option::Option<&Self::Args> {
142 self.#crates_field_ident.get(key).map(|c| &c.#args_field_ident)
143 }
144
145 fn quirks_for(&self, key: &str) -> &[Self::Quirk] {
146 self.#crates_field_ident
147 .get(key)
148 .map(|c| c.quirks.as_slice())
149 .unwrap_or(&[])
150 }
151 }
152 };
153
154 TokenStream::from(expanded)
155}
156
157#[proc_macro_derive(QuirkRegistry, attributes(quirks))]
167pub fn derive_quirk_registry(input: TokenStream) -> TokenStream {
168 let input = parse_macro_input!(input as DeriveInput);
169 let name = &input.ident;
170
171 let mut enum_name: Option<String> = None;
172 let mut registry_fn: Option<String> = None;
173 for attr in &input.attrs {
174 if !attr.path().is_ident("quirks") {
175 continue;
176 }
177 let Meta::List(list) = &attr.meta else {
178 continue;
179 };
180 let _ = list.parse_nested_meta(|meta| {
181 let Some(ident) = meta.path.get_ident() else {
182 return Ok(());
183 };
184 let value: Lit = meta.value()?.parse()?;
185 let Lit::Str(s) = value else {
186 return Ok(());
187 };
188 let v = s.value();
189 match ident.to_string().as_str() {
190 "enum_name" => enum_name = Some(v),
191 "registry_fn" => registry_fn = Some(v),
192 _ => {}
193 }
194 Ok(())
195 });
196 }
197 let enum_ty = match enum_name {
198 Some(t) => syn::parse_str::<syn::Type>(&t).expect("invalid `enum_name`"),
199 None => {
200 return TokenStream::from(quote! {
201 compile_error!("QuirkRegistry requires `#[quirks(enum_name = \"<EnumName>\", registry_fn = \"<path>\")]`");
202 });
203 }
204 };
205 let reg_path = match registry_fn {
206 Some(t) => syn::parse_str::<syn::Path>(&t).expect("invalid `registry_fn`"),
207 None => {
208 return TokenStream::from(quote! {
209 compile_error!("QuirkRegistry requires `#[quirks(enum_name = \"<EnumName>\", registry_fn = \"<path>\")]`");
210 });
211 }
212 };
213
214 let expanded = quote! {
215 impl ::gen_types::QuirkRegistry for #name {
216 type Quirk = #enum_ty;
217
218 fn registry() -> ::std::vec::Vec<(&'static str, ::std::vec::Vec<Self::Quirk>)> {
219 #reg_path()
220 }
221 }
222 };
223
224 TokenStream::from(expanded)
225}
226
227#[proc_macro_derive(TypedDispatcher)]
245pub fn derive_typed_dispatcher(input: TokenStream) -> TokenStream {
246 let input = parse_macro_input!(input as DeriveInput);
247 let name = &input.ident;
248
249 let Data::Enum(data) = &input.data else {
250 return TokenStream::from(quote! {
251 compile_error!("#[derive(TypedDispatcher)] only works on enums");
252 });
253 };
254
255 let mut kind_entries: Vec<proc_macro2::TokenStream> = Vec::new();
256 let mut field_entries: Vec<proc_macro2::TokenStream> = Vec::new();
257
258 for variant in &data.variants {
259 let tag = to_kebab_case(&variant.ident.to_string());
260 let fields = match &variant.fields {
261 Fields::Named(named) => named
262 .named
263 .iter()
264 .filter_map(|f| f.ident.as_ref().map(std::string::ToString::to_string))
265 .collect::<Vec<_>>(),
266 Fields::Unit => Vec::new(),
267 Fields::Unnamed(_) => {
268 let msg = format!(
269 "#[derive(TypedDispatcher)] variant `{}` uses tuple fields; only named-field and unit variants are supported (matches the serde-tagged-enum shape pleme-io requires)",
270 variant.ident
271 );
272 return TokenStream::from(quote! {
273 compile_error!(#msg);
274 });
275 }
276 };
277
278 kind_entries.push(quote! { #tag });
279 let field_strs: Vec<proc_macro2::TokenStream> =
280 fields.iter().map(|f| quote! { #f }).collect();
281 field_entries.push(quote! {
282 (#tag, ::std::vec![ #( #field_strs ),* ])
283 });
284 }
285
286 let expanded = quote! {
287 impl ::gen_types::TypedDispatcher for #name {
288 fn variant_kinds() -> ::std::vec::Vec<&'static str> {
289 ::std::vec![ #( #kind_entries ),* ]
290 }
291
292 fn variant_fields() -> ::std::vec::Vec<(&'static str, ::std::vec::Vec<&'static str>)> {
293 ::std::vec![ #( #field_entries ),* ]
294 }
295 }
296 };
297
298 TokenStream::from(expanded)
299}
300
301#[derive(Clone, Copy)]
321enum DiscriminantCase {
322 Kebab,
323 Snake,
324 Lower,
325 Title,
326}
327
328impl DiscriminantCase {
329 fn apply(self, s: &str) -> String {
330 match self {
331 DiscriminantCase::Kebab => to_kebab_case(s),
332 DiscriminantCase::Snake => discriminant_to_snake(s),
333 DiscriminantCase::Lower => s.to_ascii_lowercase(),
334 DiscriminantCase::Title => s.to_string(),
335 }
336 }
337
338 fn parse(s: &str) -> Option<Self> {
339 match s {
340 "kebab" | "kebab-case" => Some(DiscriminantCase::Kebab),
341 "snake" | "snake_case" => Some(DiscriminantCase::Snake),
342 "lower" | "lowercase" => Some(DiscriminantCase::Lower),
343 "title" | "Title" | "TitleCase" => Some(DiscriminantCase::Title),
344 _ => None,
345 }
346 }
347}
348
349fn discriminant_to_snake(s: &str) -> String {
350 let mut out = String::with_capacity(s.len() + 4);
351 for (i, c) in s.chars().enumerate() {
352 if c.is_ascii_uppercase() {
353 if i > 0 {
354 out.push('_');
355 }
356 out.push(c.to_ascii_lowercase());
357 } else {
358 out.push(c);
359 }
360 }
361 out
362}
363
364fn discriminant_variant_pattern(v: &syn::Variant) -> proc_macro2::TokenStream {
365 let name = &v.ident;
366 match &v.fields {
367 Fields::Unit => quote! { Self::#name },
368 Fields::Unnamed(_) => quote! { Self::#name(..) },
369 Fields::Named(_) => quote! { Self::#name { .. } },
370 }
371}
372
373fn discriminant_variant_explicit_name(v: &syn::Variant) -> Option<String> {
374 for attr in &v.attrs {
375 if !attr.path().is_ident("discriminant") {
376 continue;
377 }
378 let mut out = None;
379 let _ = attr.parse_nested_meta(|meta| {
380 if meta.path.is_ident("name") {
381 let value = meta.value()?;
382 let s: syn::LitStr = value.parse()?;
383 out = Some(s.value());
384 }
385 Ok(())
386 });
387 if out.is_some() {
388 return out;
389 }
390 }
391 None
392}
393
394#[proc_macro_derive(Discriminant, attributes(discriminant))]
418pub fn derive_discriminant(input: TokenStream) -> TokenStream {
419 let input = parse_macro_input!(input as DeriveInput);
420 let enum_name = input.ident.clone();
421
422 let Data::Enum(de) = input.data.clone() else {
423 return syn::Error::new_spanned(
424 &enum_name,
425 "#[derive(Discriminant)] is only valid on enums",
426 )
427 .to_compile_error()
428 .into();
429 };
430
431 let mut method = "discriminant".to_string();
432 let mut case = DiscriminantCase::Kebab;
433 let mut also_display = false;
434 for attr in &input.attrs {
435 if !attr.path().is_ident("discriminant") {
436 continue;
437 }
438 let _ = attr.parse_nested_meta(|meta| {
439 if meta.path.is_ident("method") {
440 let value = meta.value()?;
441 let s: syn::LitStr = value.parse()?;
442 method = s.value();
443 } else if meta.path.is_ident("case") {
444 let value = meta.value()?;
445 let s: syn::LitStr = value.parse()?;
446 if let Some(c) = DiscriminantCase::parse(&s.value()) {
447 case = c;
448 }
449 } else if meta.path.is_ident("also_display") {
450 also_display = true;
451 }
452 Ok(())
453 });
454 }
455 let method_ident = syn::Ident::new(&method, proc_macro2::Span::call_site());
456
457 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
458
459 let arms: Vec<proc_macro2::TokenStream> = de
460 .variants
461 .iter()
462 .map(|v| {
463 let pattern = discriminant_variant_pattern(v);
464 let name_str = discriminant_variant_explicit_name(v)
465 .unwrap_or_else(|| case.apply(&v.ident.to_string()));
466 quote! { #pattern => #name_str }
467 })
468 .collect();
469
470 let display_impl = if also_display {
471 quote! {
472 impl #impl_generics ::core::fmt::Display for #enum_name #ty_generics #where_clause {
473 fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
474 f.write_str(self.#method_ident())
475 }
476 }
477 }
478 } else {
479 quote! {}
480 };
481
482 let expanded = quote! {
483 impl #impl_generics #enum_name #ty_generics #where_clause {
484 pub const fn #method_ident(&self) -> &'static str {
490 match self {
491 #(#arms),*
492 }
493 }
494 }
495 #display_impl
496 };
497
498 expanded.into()
499}
500
501#[proc_macro_derive(FromStrKind, attributes(from_str_kind))]
519pub fn derive_from_str_kind(input: TokenStream) -> TokenStream {
520 let input = parse_macro_input!(input as DeriveInput);
521 let enum_name = input.ident.clone();
522
523 let Data::Enum(de) = input.data.clone() else {
524 return syn::Error::new_spanned(
525 &enum_name,
526 "#[derive(FromStrKind)] is only valid on enums",
527 )
528 .to_compile_error()
529 .into();
530 };
531
532 let mut case = DiscriminantCase::Kebab;
533 let mut error_name = format!("{enum_name}ParseError");
534 for attr in &input.attrs {
535 if !attr.path().is_ident("from_str_kind") {
536 continue;
537 }
538 let _ = attr.parse_nested_meta(|meta| {
539 if meta.path.is_ident("case") {
540 let value = meta.value()?;
541 let s: syn::LitStr = value.parse()?;
542 if let Some(c) = DiscriminantCase::parse(&s.value()) {
543 case = c;
544 }
545 } else if meta.path.is_ident("error") {
546 let value = meta.value()?;
547 let s: syn::LitStr = value.parse()?;
548 error_name = s.value();
549 }
550 Ok(())
551 });
552 }
553 let error_ident = syn::Ident::new(&error_name, proc_macro2::Span::call_site());
554
555 let mut arms: Vec<proc_macro2::TokenStream> = Vec::new();
556 let mut known_strings: Vec<String> = Vec::new();
557 for v in &de.variants {
558 if !matches!(v.fields, Fields::Unit) {
559 return syn::Error::new_spanned(
560 &v.ident,
561 "#[derive(FromStrKind)] requires all variants to be unit variants (no data payloads)",
562 )
563 .to_compile_error()
564 .into();
565 }
566 let v_ident = &v.ident;
567 let explicit = v.attrs.iter().find_map(|attr| {
568 if !attr.path().is_ident("from_str_kind") {
569 return None;
570 }
571 let mut out = None;
572 let _ = attr.parse_nested_meta(|meta| {
573 if meta.path.is_ident("name") {
574 let value = meta.value()?;
575 let s: syn::LitStr = value.parse()?;
576 out = Some(s.value());
577 }
578 Ok(())
579 });
580 out
581 });
582 let name_str = explicit.unwrap_or_else(|| case.apply(&v_ident.to_string()));
583 known_strings.push(name_str.clone());
584 arms.push(quote! { #name_str => Ok(Self::#v_ident) });
585 }
586
587 let known_list = known_strings.join(" | ");
588 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
589
590 let expanded = quote! {
591 #[derive(Debug, Clone, PartialEq, Eq)]
593 pub struct #error_ident {
594 pub input: ::std::string::String,
595 }
596
597 impl ::core::fmt::Display for #error_ident {
598 fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
599 write!(
600 f,
601 "unknown variant {input:?}; expected one of: {known}",
602 input = self.input,
603 known = #known_list,
604 )
605 }
606 }
607
608 impl ::std::error::Error for #error_ident {}
609
610 impl #impl_generics ::core::str::FromStr for #enum_name #ty_generics #where_clause {
611 type Err = #error_ident;
612 fn from_str(s: &str) -> ::core::result::Result<Self, Self::Err> {
613 match s {
614 #(#arms),*,
615 other => Err(#error_ident { input: other.to_string() }),
616 }
617 }
618 }
619 };
620
621 expanded.into()
622}
623
624fn is_variant_method_name(v: &syn::Variant) -> syn::Ident {
625 let explicit = v.attrs.iter().find_map(|attr| {
626 if !attr.path().is_ident("is_variant") {
627 return None;
628 }
629 let mut out = None;
630 let _ = attr.parse_nested_meta(|meta| {
631 if meta.path.is_ident("name") {
632 let value = meta.value()?;
633 let s: syn::LitStr = value.parse()?;
634 out = Some(s.value());
635 }
636 Ok(())
637 });
638 out
639 });
640 let snake = explicit.unwrap_or_else(|| discriminant_to_snake(&v.ident.to_string()));
641 syn::Ident::new(&format!("is_{snake}"), proc_macro2::Span::call_site())
642}
643
644#[proc_macro_derive(IsVariant, attributes(is_variant))]
657pub fn derive_is_variant(input: TokenStream) -> TokenStream {
658 let input = parse_macro_input!(input as DeriveInput);
659 let enum_name = input.ident.clone();
660
661 let Data::Enum(de) = input.data.clone() else {
662 return syn::Error::new_spanned(&enum_name, "#[derive(IsVariant)] is only valid on enums")
663 .to_compile_error()
664 .into();
665 };
666
667 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
668
669 let methods: Vec<proc_macro2::TokenStream> = de
670 .variants
671 .iter()
672 .map(|v| {
673 let pattern = discriminant_variant_pattern(v);
674 let method_name = is_variant_method_name(v);
675 quote! {
676 pub const fn #method_name(&self) -> bool {
677 matches!(self, #pattern)
678 }
679 }
680 })
681 .collect();
682
683 let expanded = quote! {
684 impl #impl_generics #enum_name #ty_generics #where_clause {
685 #(#methods)*
686 }
687 };
688
689 expanded.into()
690}
691
692#[proc_macro_derive(BackendError, attributes(backend_error))]
760pub fn derive_backend_error(input: TokenStream) -> TokenStream {
761 let input = parse_macro_input!(input as DeriveInput);
762 let enum_name = input.ident.clone();
763
764 let Data::Enum(de) = input.data.clone() else {
765 return syn::Error::new_spanned(
766 &enum_name,
767 "#[derive(BackendError)] is only valid on enums",
768 )
769 .to_compile_error()
770 .into();
771 };
772
773 let mut trait_path: syn::Path = syn::parse_quote!(BackendError);
774 let mut kind_method = "discriminant".to_string();
775 for attr in &input.attrs {
776 if !attr.path().is_ident("backend_error") {
777 continue;
778 }
779 let _ = attr.parse_nested_meta(|meta| {
780 if meta.path.is_ident("trait_path") {
781 let value = meta.value()?;
782 let s: syn::LitStr = value.parse()?;
783 if let Ok(p) = syn::parse_str::<syn::Path>(&s.value()) {
784 trait_path = p;
785 }
786 } else if meta.path.is_ident("kind_method") {
787 let value = meta.value()?;
788 let s: syn::LitStr = value.parse()?;
789 kind_method = s.value();
790 }
791 Ok(())
792 });
793 }
794 let kind_method_ident = syn::Ident::new(&kind_method, proc_macro2::Span::call_site());
795
796 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
797
798 let mut transient_patterns: Vec<proc_macro2::TokenStream> = Vec::new();
799 let mut auth_patterns: Vec<proc_macro2::TokenStream> = Vec::new();
800 for v in &de.variants {
801 let mut tags: std::collections::HashSet<String> = std::collections::HashSet::new();
802 for attr in &v.attrs {
803 if !attr.path().is_ident("backend_error") {
804 continue;
805 }
806 let _ = attr.parse_nested_meta(|meta| {
807 if let Some(ident) = meta.path.get_ident() {
808 tags.insert(ident.to_string());
809 }
810 Ok(())
811 });
812 }
813 let pattern = discriminant_variant_pattern(v);
814 if tags.contains("transient") {
815 transient_patterns.push(pattern.clone());
816 }
817 if tags.contains("auth") {
818 auth_patterns.push(pattern.clone());
819 }
820 }
821
822 let is_retryable_body = if transient_patterns.is_empty() {
823 quote! { false }
824 } else {
825 quote! { matches!(self, #(#transient_patterns)|*) }
826 };
827 let is_auth_failure_body = if auth_patterns.is_empty() {
828 quote! { false }
829 } else {
830 quote! { matches!(self, #(#auth_patterns)|*) }
831 };
832
833 let expanded = quote! {
834 impl #impl_generics #trait_path for #enum_name #ty_generics #where_clause {
835 fn is_retryable(&self) -> bool {
836 #is_retryable_body
837 }
838
839 fn is_auth_failure(&self) -> bool {
840 #is_auth_failure_body
841 }
842
843 fn kind(&self) -> &'static str {
844 self.#kind_method_ident()
845 }
846 }
847 };
848
849 expanded.into()
850}
851
852#[proc_macro_derive(OutcomeLattice, attributes(outcome_lattice, outcome))]
906pub fn derive_outcome_lattice(input: TokenStream) -> TokenStream {
907 let input = parse_macro_input!(input as DeriveInput);
908 let enum_name = input.ident.clone();
909
910 let Data::Enum(de) = input.data.clone() else {
911 return syn::Error::new_spanned(
912 &enum_name,
913 "#[derive(OutcomeLattice)] is only valid on enums",
914 )
915 .to_compile_error()
916 .into();
917 };
918
919 let mut trait_path: syn::Path = syn::parse_quote!(OutcomeLattice);
920 for attr in &input.attrs {
921 if !attr.path().is_ident("outcome_lattice") {
922 continue;
923 }
924 let _ = attr.parse_nested_meta(|meta| {
925 if meta.path.is_ident("trait_path") {
926 let value = meta.value()?;
927 let s: syn::LitStr = value.parse()?;
928 if let Ok(p) = syn::parse_str::<syn::Path>(&s.value()) {
929 trait_path = p;
930 }
931 }
932 Ok(())
933 });
934 }
935
936 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
937
938 let mut severity_arms: Vec<proc_macro2::TokenStream> = Vec::new();
939 let mut baseline_variant: Option<syn::Ident> = None;
940 for v in &de.variants {
941 let v_ident = &v.ident;
942 let pattern = discriminant_variant_pattern(v);
943 let mut sev: u32 = 0;
944 let mut is_baseline = false;
945 for attr in &v.attrs {
946 if !attr.path().is_ident("outcome") {
947 continue;
948 }
949 let _ = attr.parse_nested_meta(|meta| {
950 if meta.path.is_ident("severity") {
951 let value = meta.value()?;
952 let lit: syn::LitInt = value.parse()?;
953 sev = lit.base10_parse::<u32>().unwrap_or(0);
954 } else if meta.path.is_ident("baseline") {
955 is_baseline = true;
956 }
957 Ok(())
958 });
959 }
960 if is_baseline {
961 if !matches!(v.fields, Fields::Unit) {
962 return syn::Error::new_spanned(
963 v_ident,
964 "#[outcome(baseline)] requires a unit variant",
965 )
966 .to_compile_error()
967 .into();
968 }
969 if baseline_variant.is_some() {
970 return syn::Error::new_spanned(
971 v_ident,
972 "exactly one variant may carry #[outcome(baseline)]",
973 )
974 .to_compile_error()
975 .into();
976 }
977 baseline_variant = Some(v_ident.clone());
978 }
979 let lit = syn::LitInt::new(&sev.to_string(), proc_macro2::Span::call_site());
980 severity_arms.push(quote! { #pattern => #lit });
981 }
982
983 let Some(baseline_ident) = baseline_variant else {
984 return syn::Error::new_spanned(
985 &enum_name,
986 "exactly one variant must carry #[outcome(baseline)] to derive OutcomeLattice",
987 )
988 .to_compile_error()
989 .into();
990 };
991
992 let expanded = quote! {
993 impl #impl_generics #trait_path for #enum_name #ty_generics #where_clause {
994 fn severity(&self) -> u32 {
995 match self {
996 #(#severity_arms),*
997 }
998 }
999
1000 fn baseline() -> Self {
1001 Self::#baseline_ident
1002 }
1003 }
1004 };
1005
1006 expanded.into()
1007}
1008
1009fn to_kebab_case(s: &str) -> String {
1014 let mut out = String::with_capacity(s.len() + 4);
1015 let mut prev_lower = false;
1016 let mut prev_digit = false;
1017 for ch in s.chars() {
1018 if ch.is_ascii_uppercase() {
1019 if prev_lower || prev_digit {
1020 out.push('-');
1021 }
1022 for c in ch.to_lowercase() {
1023 out.push(c);
1024 }
1025 prev_lower = false;
1026 prev_digit = false;
1027 } else if ch.is_ascii_digit() {
1028 out.push(ch);
1029 prev_lower = false;
1030 prev_digit = true;
1031 } else {
1032 out.push(ch);
1033 prev_lower = true;
1034 prev_digit = false;
1035 }
1036 }
1037 out
1038}
1039
1040#[proc_macro_attribute]
1092pub fn fsm(args: TokenStream, item: TokenStream) -> TokenStream {
1093 let args = parse_macro_input!(args as FsmArgs);
1094 let item = parse_macro_input!(item as syn::ItemEnum);
1095 let name = &item.ident;
1096 let label = &args.label;
1097
1098 quote! {
1099 #[derive(
1100 ::core::clone::Clone,
1101 ::core::fmt::Debug,
1102 ::core::cmp::PartialEq,
1103 ::core::cmp::Eq,
1104 ::serde::Serialize,
1105 ::serde::Deserialize,
1106 ::gen_macros::TypedDispatcher,
1107 ::gen_macros::Discriminant,
1108 ::gen_macros::IsVariant,
1109 )]
1110 #[serde(tag = "kind", rename_all = "kebab-case")]
1111 #item
1112
1113 ::gen_platform::register_dispatcher!(#label, #name);
1114 }
1115 .into()
1116}
1117
1118struct FsmArgs {
1119 label: String,
1120}
1121
1122impl syn::parse::Parse for FsmArgs {
1123 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
1124 let kw: syn::Ident = input.parse()?;
1125 if kw != "label" {
1126 return Err(syn::Error::new(
1127 kw.span(),
1128 "expected `label = \"<catalog-label>\"`",
1129 ));
1130 }
1131 input.parse::<syn::Token![=]>()?;
1132 let lit: syn::LitStr = input.parse()?;
1133 Ok(Self { label: lit.value() })
1134 }
1135}