1use proc_macro::TokenStream;
21use quote::quote;
22use syn::{
23 Attribute, Data, DeriveInput, Fields, Ident, Variant, parse_macro_input, spanned::Spanned,
24};
25
26#[proc_macro_derive(HasWorkflowId, attributes(workflow_id))]
31pub fn derive_has_workflow_id(input: TokenStream) -> TokenStream {
32 let input = parse_macro_input!(input as DeriveInput);
33
34 match derive_has_workflow_id_impl(input) {
35 Ok(tokens) => tokens.into(),
36 Err(err) => err.to_compile_error().into(),
37 }
38}
39
40fn derive_has_workflow_id_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
41 let name = &input.ident;
42
43 let default_field = get_workflow_id_attr(&input.attrs)?;
45
46 let data = match &input.data {
47 Data::Enum(data) => data,
48 _ => {
49 return Err(syn::Error::new(
50 input.span(),
51 "HasWorkflowId can only be derived for enums",
52 ));
53 }
54 };
55
56 let mut match_arms = Vec::new();
57
58 for variant in &data.variants {
59 let field_name = get_variant_workflow_id(variant, &default_field)?;
60 let arm = generate_match_arm(name, variant, &field_name)?;
61 match_arms.push(arm);
62 }
63
64 Ok(quote! {
65 impl ::ironflow::HasWorkflowId for #name {
66 fn workflow_id(&self) -> ::ironflow::WorkflowId {
67 match self {
68 #(#match_arms)*
69 }
70 }
71 }
72 })
73}
74
75fn get_workflow_id_attr(attrs: &[Attribute]) -> syn::Result<Option<Ident>> {
77 for attr in attrs {
78 if attr.path().is_ident("workflow_id") {
79 let ident: Ident = attr.parse_args()?;
80 return Ok(Some(ident));
81 }
82 }
83 Ok(None)
84}
85
86fn get_variant_workflow_id(variant: &Variant, default_field: &Option<Ident>) -> syn::Result<Ident> {
88 if let Some(field) = get_workflow_id_attr(&variant.attrs)? {
90 return Ok(field);
91 }
92
93 if let Some(field) = default_field {
95 return Ok(field.clone());
96 }
97
98 Err(syn::Error::new(
100 variant.span(),
101 format!(
102 "Variant `{}` has no #[workflow_id(field)] attribute and no default is set. \
103 Either add #[workflow_id(field_name)] to this variant or add a default \
104 #[workflow_id(field_name)] attribute to the enum.",
105 variant.ident
106 ),
107 ))
108}
109
110fn generate_match_arm(
112 enum_name: &Ident,
113 variant: &Variant,
114 field_name: &Ident,
115) -> syn::Result<proc_macro2::TokenStream> {
116 let variant_name = &variant.ident;
117
118 match &variant.fields {
119 Fields::Named(fields) => {
120 let field_exists = fields
122 .named
123 .iter()
124 .any(|f| f.ident.as_ref() == Some(field_name));
125
126 if !field_exists {
127 return Err(syn::Error::new(
128 variant.span(),
129 format!(
130 "Field `{}` not found in variant `{}`. Available fields: {}",
131 field_name,
132 variant_name,
133 fields
134 .named
135 .iter()
136 .filter_map(|f| f.ident.as_ref())
137 .map(|i| i.to_string())
138 .collect::<Vec<_>>()
139 .join(", ")
140 ),
141 ));
142 }
143
144 Ok(quote! {
145 #enum_name::#variant_name { #field_name, .. } => ::ironflow::WorkflowId::new(#field_name),
146 })
147 }
148 Fields::Unnamed(_) => Err(syn::Error::new(
149 variant.span(),
150 "HasWorkflowId derive does not support tuple variants. Use named fields instead.",
151 )),
152 Fields::Unit => Err(syn::Error::new(
153 variant.span(),
154 "HasWorkflowId derive does not support unit variants. All variants must have fields.",
155 )),
156 }
157}