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::{
27    MachinePath, StateModulePath, ensure_machine_loaded_by_name, ensure_state_enum_loaded,
28};
29use macro_registry::callsite::current_module_path;
30use proc_macro::TokenStream;
31use syn::{Item, ItemImpl, parse_macro_input};
32
33/// Define the legal lifecycle phases for a Statum machine.
34///
35/// Apply `#[state]` to an enum with unit variants and single-field tuple
36/// variants. Statum generates one marker type per variant plus the state-family
37/// traits used by `#[machine]`, `#[transition]`, and `#[validators]`.
38#[proc_macro_attribute]
39pub fn state(_attr: TokenStream, item: TokenStream) -> TokenStream {
40    let input = parse_macro_input!(item as Item);
41    let input = match input {
42        Item::Enum(item_enum) => item_enum,
43        other => return invalid_state_target_error(&other).into(),
44    };
45
46    // Validate the enum before proceeding
47    if let Some(error) = validate_state_enum(&input) {
48        return error.into();
49    }
50
51    let enum_info = match EnumInfo::from_item_enum(&input) {
52        Ok(info) => info,
53        Err(err) => return err.to_compile_error().into(),
54    };
55
56    // Store metadata in `state_enum_map`
57    store_state_enum(&enum_info);
58
59    // Generate structs and implementations dynamically
60    let expanded = generate_state_impls(&enum_info.module_path);
61
62    TokenStream::from(expanded)
63}
64
65/// Define a typed machine that carries durable context across states.
66///
67/// Apply `#[machine]` to a struct whose first generic parameter is the
68/// `#[state]` enum family. Statum generates the typed machine surface, builders,
69/// the machine-scoped `machine::State` enum, and helper items such as
70/// `machine::Fields` for heterogeneous batch rebuilds.
71#[proc_macro_attribute]
72pub fn machine(_attr: TokenStream, item: TokenStream) -> TokenStream {
73    let input = parse_macro_input!(item as Item);
74    let input = match input {
75        Item::Struct(item_struct) => item_struct,
76        other => return invalid_machine_target_error(&other).into(),
77    };
78
79    let machine_info = MachineInfo::from_item_struct(&input);
80
81    // Validate the struct before proceeding
82    if let Some(error) = validate_machine_struct(&input, &machine_info) {
83        return error.into();
84    }
85
86    // Store metadata in `machine_map`
87    store_machine_struct(&machine_info);
88
89    // Generate any required structs or implementations dynamically
90    let expanded = generate_machine_impls(&machine_info);
91
92    TokenStream::from(expanded)
93}
94
95/// Validate and generate legal transitions for one source state.
96///
97/// Apply `#[transition]` to an `impl Machine<CurrentState>` block. Each method
98/// must consume `self` and return a legal `Machine<NextState>` shape or a
99/// supported wrapper around it, such as `Result<Machine<NextState>, E>`.
100#[proc_macro_attribute]
101pub fn transition(
102    _attr: proc_macro::TokenStream,
103    item: proc_macro::TokenStream,
104) -> proc_macro::TokenStream {
105    let input = parse_macro_input!(item as ItemImpl);
106
107    // -- Step 1: Parse
108    let tr_impl = match parse_transition_impl(&input) {
109        Ok(parsed) => parsed,
110        Err(err) => return err.into(),
111    };
112
113    let module_path = current_module_path();
114
115    let state_path: StateModulePath = module_path.clone().into();
116    let machine_path: MachinePath = module_path.clone().into();
117    let _ = ensure_state_enum_loaded(&state_path);
118    let machine_info_owned = ensure_machine_loaded_by_name(&machine_path, &tr_impl.machine_name);
119    let machine_info = match machine_info_owned.as_ref() {
120        Some(info) => info,
121        None => {
122            return missing_transition_machine_error(
123                &tr_impl.machine_name,
124                &module_path,
125                tr_impl.machine_span,
126            )
127            .into();
128        }
129    };
130
131    if let Some(err) = validate_transition_functions(&tr_impl, machine_info) {
132        return err.into();
133    }
134
135    // -- Step 3: Generate new code
136    let expanded = generate_transition_impl(&input, &tr_impl, machine_info, &module_path);
137
138    // Combine expanded code with the original `impl` if needed
139    // or simply return the expanded code
140    expanded.into()
141}
142
143/// Rebuild typed machines from persisted data.
144///
145/// Apply `#[validators(Machine)]` to an `impl PersistedRow` block. Statum
146/// expects one `is_{state}` method per state variant and generates
147/// `into_machine()`, `.into_machines()`, and `.into_machines_by(...)` helpers
148/// for typed rehydration.
149#[proc_macro_attribute]
150pub fn validators(attr: TokenStream, item: TokenStream) -> TokenStream {
151    let module_path = current_module_path();
152    parse_validators(attr, item, &module_path)
153}