Skip to main content

statum_macros/
lib.rs

1//! Proc-macro implementation crate for Statum.
2//!
3//! Most users should depend on [`statum`](https://docs.rs/statum), which
4//! re-exports these macros with the public-facing documentation. This crate
5//! exists so the macro expansion logic can stay separate from the stable runtime
6//! traits in `statum-core`.
7//!
8//! The public macros are:
9//!
10//! - [`state`] for declaring legal lifecycle phases
11//! - [`machine`] for declaring the typed machine and durable context
12//! - [`transition`] for validating legal transition impls
13//! - [`validators`] for rebuilding typed machines from persisted data
14
15moddef::moddef!(
16    flat (pub) mod {
17    },
18    flat (pub(crate)) mod {
19        state,
20        machine,
21        transition,
22        validators
23    }
24);
25
26use crate::{MachinePath, ensure_machine_loaded_by_name};
27use macro_registry::callsite::current_module_path_opt;
28use proc_macro::TokenStream;
29use proc_macro2::Span;
30use syn::{Item, ItemImpl, parse_macro_input};
31
32/// Define the legal lifecycle phases for a Statum machine.
33///
34/// Apply `#[state]` to an enum with unit variants and single-field tuple
35/// variants. Statum generates one marker type per variant plus the state-family
36/// traits used by `#[machine]`, `#[transition]`, and `#[validators]`.
37#[proc_macro_attribute]
38pub fn state(_attr: TokenStream, item: TokenStream) -> TokenStream {
39    let input = parse_macro_input!(item as Item);
40    let input = match input {
41        Item::Enum(item_enum) => item_enum,
42        other => return invalid_state_target_error(&other).into(),
43    };
44
45    // Validate the enum before proceeding
46    if let Some(error) = validate_state_enum(&input) {
47        return error.into();
48    }
49
50    let enum_info = match EnumInfo::from_item_enum(&input) {
51        Ok(info) => info,
52        Err(err) => return err.to_compile_error().into(),
53    };
54
55    // Store metadata in `state_enum_map`
56    store_state_enum(&enum_info);
57
58    // Generate structs and implementations dynamically
59    let expanded = generate_state_impls(&enum_info.module_path);
60
61    TokenStream::from(expanded)
62}
63
64/// Define a typed machine that carries durable context across states.
65///
66/// Apply `#[machine]` to a struct whose first generic parameter is the
67/// `#[state]` enum family. Statum generates the typed machine surface, builders,
68/// the machine-scoped `machine::State` enum, and helper items such as
69/// `machine::Fields` for heterogeneous batch rebuilds.
70#[proc_macro_attribute]
71pub fn machine(_attr: TokenStream, item: TokenStream) -> TokenStream {
72    let input = parse_macro_input!(item as Item);
73    let input = match input {
74        Item::Struct(item_struct) => item_struct,
75        other => return invalid_machine_target_error(&other).into(),
76    };
77
78    let machine_info = match MachineInfo::from_item_struct(&input) {
79        Ok(info) => info,
80        Err(err) => return err.to_compile_error().into(),
81    };
82
83    // Validate the struct before proceeding
84    if let Some(error) = validate_machine_struct(&input, &machine_info) {
85        return error.into();
86    }
87
88    // Store metadata in `machine_map`
89    store_machine_struct(&machine_info);
90
91    // Generate any required structs or implementations dynamically
92    let expanded = generate_machine_impls(&machine_info, &input);
93
94    TokenStream::from(expanded)
95}
96
97/// Validate and generate legal transitions for one source state.
98///
99/// Apply `#[transition]` to an `impl Machine<CurrentState>` block. Each method
100/// must consume `self` and return a legal `Machine<NextState>` shape or a
101/// supported wrapper around it, such as `Result<Machine<NextState>, E>`.
102#[proc_macro_attribute]
103pub fn transition(
104    _attr: proc_macro::TokenStream,
105    item: proc_macro::TokenStream,
106) -> proc_macro::TokenStream {
107    let input = parse_macro_input!(item as ItemImpl);
108
109    // -- Step 1: Parse
110    let tr_impl = match parse_transition_impl(&input) {
111        Ok(parsed) => parsed,
112        Err(err) => return err.into(),
113    };
114
115    let module_path = match resolved_current_module_path(tr_impl.machine_span, "#[transition]") {
116        Ok(path) => path,
117        Err(err) => return err,
118    };
119
120    let machine_path: MachinePath = module_path.clone().into();
121    let machine_info_owned = ensure_machine_loaded_by_name(&machine_path, &tr_impl.machine_name);
122    let machine_info = match machine_info_owned.as_ref() {
123        Some(info) => info,
124        None => {
125            return missing_transition_machine_error(
126                &tr_impl.machine_name,
127                &module_path,
128                tr_impl.machine_span,
129            )
130            .into();
131        }
132    };
133
134    if let Some(err) = validate_transition_functions(&tr_impl, machine_info) {
135        return err.into();
136    }
137
138    // -- Step 3: Generate new code
139    let expanded = generate_transition_impl(&input, &tr_impl, machine_info, &module_path);
140
141    // Combine expanded code with the original `impl` if needed
142    // or simply return the expanded code
143    expanded.into()
144}
145
146/// Rebuild typed machines from persisted data.
147///
148/// Apply `#[validators(Machine)]` to an `impl PersistedRow` block. Statum
149/// expects one `is_{state}` method per state variant and generates
150/// `into_machine()`, `.into_machines()`, and `.into_machines_by(...)` helpers
151/// for typed rehydration.
152#[proc_macro_attribute]
153pub fn validators(attr: TokenStream, item: TokenStream) -> TokenStream {
154    let module_path = match resolved_current_module_path(Span::call_site(), "#[validators]") {
155        Ok(path) => path,
156        Err(err) => return err,
157    };
158    parse_validators(attr, item, &module_path)
159}
160
161fn resolved_current_module_path(span: Span, macro_name: &str) -> Result<String, TokenStream> {
162    current_module_path_opt().ok_or_else(|| {
163        let message = format!(
164            "Internal error: could not resolve the module path for `{macro_name}` at this call site."
165        );
166        quote::quote_spanned! { span =>
167            compile_error!(#message);
168        }
169        .into()
170    })
171}