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
15#[cfg(doctest)]
16#[doc = include_str!("../README.md")]
17mod readme_doctests {}
18
19mod syntax;
20
21moddef::moddef!(
22    flat (pub) mod {
23    },
24    flat (pub(crate)) mod {
25        state,
26        machine,
27        transition,
28        validators
29    }
30);
31
32pub(crate) use syntax::{
33    ItemTarget, ModulePath, SourceFingerprint, crate_root_for_file, current_crate_root,
34    extract_derives, source_file_fingerprint,
35};
36
37use crate::{
38    LoadedMachineLookupFailure, MachinePath, ambiguous_transition_machine_error,
39    ambiguous_transition_machine_fallback_error, lookup_loaded_machine_in_module,
40    lookup_unique_loaded_machine_by_name,
41};
42use macro_registry::callsite::current_module_path_opt;
43use proc_macro::TokenStream;
44use proc_macro2::Span;
45use syn::{Item, ItemImpl, parse_macro_input};
46
47/// Define the legal lifecycle phases for a Statum machine.
48///
49/// Apply `#[state]` to an enum with unit variants and single-field tuple
50/// variants. Statum generates one marker type per variant plus the state-family
51/// traits used by `#[machine]`, `#[transition]`, and `#[validators]`.
52#[proc_macro_attribute]
53pub fn state(_attr: TokenStream, item: TokenStream) -> TokenStream {
54    let input = parse_macro_input!(item as Item);
55    let input = match input {
56        Item::Enum(item_enum) => item_enum,
57        other => return invalid_state_target_error(&other).into(),
58    };
59
60    // Validate the enum before proceeding
61    if let Some(error) = validate_state_enum(&input) {
62        return error.into();
63    }
64
65    let enum_info = match EnumInfo::from_item_enum(&input) {
66        Ok(info) => info,
67        Err(err) => return err.to_compile_error().into(),
68    };
69
70    // Store metadata in `state_enum_map`
71    store_state_enum(&enum_info);
72
73    // Generate structs and implementations dynamically
74    let expanded = generate_state_impls(&enum_info);
75
76    TokenStream::from(expanded)
77}
78
79/// Define a typed machine that carries durable context across states.
80///
81/// Apply `#[machine]` to a struct whose first generic parameter is the
82/// `#[state]` enum family. Statum generates the typed machine surface, builders,
83/// the machine-scoped `machine::SomeState` enum, a compatibility alias
84/// `machine::State = machine::SomeState`, and helper items such as
85/// `machine::Fields` for heterogeneous batch rebuilds.
86#[proc_macro_attribute]
87pub fn machine(_attr: TokenStream, item: TokenStream) -> TokenStream {
88    let input = parse_macro_input!(item as Item);
89    let input = match input {
90        Item::Struct(item_struct) => item_struct,
91        other => return invalid_machine_target_error(&other).into(),
92    };
93
94    let machine_info = match MachineInfo::from_item_struct(&input) {
95        Ok(info) => info,
96        Err(err) => return err.to_compile_error().into(),
97    };
98
99    // Validate the struct before proceeding
100    if let Some(error) = validate_machine_struct(&input, &machine_info) {
101        return error.into();
102    }
103
104    // Store metadata in `machine_map`
105    store_machine_struct(&machine_info);
106
107    // Generate any required structs or implementations dynamically
108    let expanded = generate_machine_impls(&machine_info, &input);
109
110    TokenStream::from(expanded)
111}
112
113/// Validate and generate legal transitions for one source state.
114///
115/// Apply `#[transition]` to an `impl Machine<CurrentState>` block. Each method
116/// must consume `self` and return a legal `Machine<NextState>` shape or a
117/// supported wrapper around it, such as `Result<Machine<NextState>, E>`.
118#[proc_macro_attribute]
119pub fn transition(
120    _attr: proc_macro::TokenStream,
121    item: proc_macro::TokenStream,
122) -> proc_macro::TokenStream {
123    let input = parse_macro_input!(item as ItemImpl);
124
125    // -- Step 1: Parse
126    let tr_impl = match parse_transition_impl(&input) {
127        Ok(parsed) => parsed,
128        Err(err) => return err.into(),
129    };
130
131    let module_path = match resolved_current_module_path(tr_impl.machine_span, "#[transition]") {
132        Ok(path) => path,
133        Err(err) => return err,
134    };
135
136    let machine_path: MachinePath = module_path.clone().into();
137    let machine_info_owned =
138        match lookup_loaded_machine_in_module(&machine_path, &tr_impl.machine_name) {
139            Ok(info) => Some(info),
140            Err(LoadedMachineLookupFailure::Ambiguous(candidates)) => {
141                return ambiguous_transition_machine_error(
142                    &tr_impl.machine_name,
143                    &module_path,
144                    &candidates,
145                    tr_impl.machine_span,
146                )
147                .into();
148            }
149            Err(LoadedMachineLookupFailure::NotFound) => {
150                match lookup_unique_loaded_machine_by_name(&tr_impl.machine_name) {
151                    Ok(info) => Some(info),
152                    Err(LoadedMachineLookupFailure::Ambiguous(candidates)) => {
153                        return ambiguous_transition_machine_fallback_error(
154                            &tr_impl.machine_name,
155                            &module_path,
156                            &candidates,
157                            tr_impl.machine_span,
158                        )
159                        .into();
160                    }
161                    Err(LoadedMachineLookupFailure::NotFound) => None,
162                }
163            }
164        };
165    let machine_info = match machine_info_owned.as_ref() {
166        Some(info) => info,
167        None => {
168            return missing_transition_machine_error(
169                &tr_impl.machine_name,
170                &module_path,
171                tr_impl.machine_span,
172            )
173            .into();
174        }
175    };
176
177    if let Some(err) = validate_transition_functions(&tr_impl, machine_info) {
178        return err.into();
179    }
180
181    // -- Step 3: Generate new code
182    let expanded = generate_transition_impl(&input, &tr_impl, machine_info);
183
184    // Combine expanded code with the original `impl` if needed
185    // or simply return the expanded code
186    expanded.into()
187}
188
189/// Rebuild typed machines from persisted data.
190///
191/// Apply `#[validators(Machine)]` to an `impl PersistedRow` block. Statum
192/// expects one `is_{state}` method per state variant and generates
193/// `into_machine()`, `.into_machines()`, and `.into_machines_by(...)` helpers
194/// for typed rehydration.
195#[proc_macro_attribute]
196pub fn validators(attr: TokenStream, item: TokenStream) -> TokenStream {
197    let module_path = match resolved_current_module_path(Span::call_site(), "#[validators]") {
198        Ok(path) => path,
199        Err(err) => return err,
200    };
201    parse_validators(attr, item, &module_path)
202}
203
204fn resolved_current_module_path(span: Span, macro_name: &str) -> Result<String, TokenStream> {
205    current_module_path_opt().ok_or_else(|| {
206        let message = format!(
207            "Internal error: could not resolve the module path for `{macro_name}` at this call site."
208        );
209        quote::quote_spanned! { span =>
210            compile_error!(#message);
211        }
212        .into()
213    })
214}