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