ironflow_macros/
lib.rs

1//! Procedural macros for the ironflow workflow engine.
2//!
3//! # HasWorkflowId Derive Macro
4//!
5//! Automatically implements `HasWorkflowId` for enum input types.
6//!
7//! ## Usage
8//!
9//! ```ignore
10//! #[derive(HasWorkflowId)]
11//! #[workflow_id(order_id)]  // default field name for all variants
12//! enum OrderInput {
13//!     Create { order_id: String, items: Vec<Item> },
14//!     Confirm { order_id: String },
15//!     #[workflow_id(id)]  // override for this variant
16//!     Cancel { id: String },
17//! }
18//! ```
19
20use proc_macro::TokenStream;
21use quote::quote;
22use syn::{
23    Attribute, Data, DeriveInput, Fields, Ident, Variant, parse_macro_input, spanned::Spanned,
24};
25
26/// Derives `HasWorkflowId` for an enum.
27///
28/// Use `#[workflow_id(field_name)]` on the enum to set the default field,
29/// and optionally on individual variants to override.
30#[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    // Get the default field from enum-level #[workflow_id(field)]
44    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
75/// Extract the field name from `#[workflow_id(field_name)]` attribute.
76fn 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
86/// Get the workflow_id field for a variant (variant-level override or default).
87fn get_variant_workflow_id(variant: &Variant, default_field: &Option<Ident>) -> syn::Result<Ident> {
88    // Check for variant-level override
89    if let Some(field) = get_workflow_id_attr(&variant.attrs)? {
90        return Ok(field);
91    }
92
93    // Use default if available
94    if let Some(field) = default_field {
95        return Ok(field.clone());
96    }
97
98    // No default and no override - error
99    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
110/// Generate a match arm for a variant.
111fn 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            // Verify the field exists
121            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}