1use proc_macro::TokenStream;
41use quote::quote;
42use syn::{parse_macro_input, Data, DeriveInput, Fields, Lit, Meta};
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 { 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#[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#[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#[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#[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 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#[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 #[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#[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#[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#[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
1008fn 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#[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}