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 contracts;
20mod diagnostics;
21mod source;
22
23moddef::moddef!(
24    flat (pub) mod {
25    },
26    flat (pub(crate)) mod {
27        presentation,
28        state,
29        machine,
30        transition,
31        validators
32    }
33);
34
35pub(crate) use presentation::{
36    PresentationAttr, PresentationTypesAttr, parse_present_attrs, parse_presentation_types_attr,
37    strip_present_attrs,
38};
39pub(crate) use source::{
40    ItemTarget, ModulePath, SourceFingerprint, crate_root_for_file, current_crate_root,
41    extract_derives, source_file_fingerprint,
42};
43
44use crate::diagnostics::DiagnosticMessage;
45use crate::source::{current_module_path_opt, module_path_for_span, source_info_for_span};
46use proc_macro::TokenStream;
47use proc_macro2::Span;
48use syn::spanned::Spanned;
49use syn::{Item, ItemImpl, parse_macro_input};
50
51pub(crate) fn strict_introspection_enabled() -> bool {
52    cfg!(feature = "strict-introspection")
53}
54
55/// Define the legal lifecycle phases for a Statum machine.
56///
57/// Apply `#[state]` to an enum with unit variants, single-field tuple
58/// variants, or named-field variants. Statum generates one marker type per
59/// variant plus the state-family traits used by `#[machine]`, `#[transition]`,
60/// and `#[validators]`.
61#[proc_macro_attribute]
62pub fn state(attr: TokenStream, item: TokenStream) -> TokenStream {
63    if !attr.is_empty() {
64        return syn::Error::new(
65            Span::call_site(),
66            DiagnosticMessage::new("`#[state]` does not accept arguments.")
67                .found(format!("`#[state({attr})]`"))
68                .expected("`#[state] enum WorkflowState { Draft, Review(ReviewData) }`")
69                .fix("remove the attribute arguments and describe states with enum variants instead.".to_string())
70                .render(),
71        )
72        .to_compile_error()
73        .into();
74    }
75    let input = parse_macro_input!(item as Item);
76    let input = match input {
77        Item::Enum(item_enum) => item_enum,
78        other => return invalid_state_target_error(&other).into(),
79    };
80    expand_state(input).into()
81}
82
83/// Define a typed machine that carries durable context across states.
84///
85/// Apply `#[machine]` to a struct whose first generic parameter is the
86/// `#[state]` enum family. Additional type and const generics are supported
87/// after that state generic. Statum generates the typed machine surface,
88/// builders, the machine-scoped `machine::SomeState` enum, a compatibility
89/// alias `machine::State = machine::SomeState`, and helper items such as
90/// `machine::Fields` for heterogeneous batch rebuilds.
91#[proc_macro_attribute]
92pub fn machine(attr: TokenStream, item: TokenStream) -> TokenStream {
93    if !attr.is_empty() {
94        return syn::Error::new(
95            Span::call_site(),
96            DiagnosticMessage::new("`#[machine]` does not accept arguments.")
97                .found(format!("`#[machine({attr})]`"))
98                .expected("`#[machine] struct WorkflowMachine<WorkflowState> { ... }`")
99                .fix("remove the attribute arguments and link the machine to `#[state]` through its first generic parameter.".to_string())
100                .render(),
101        )
102        .to_compile_error()
103        .into();
104    }
105    let input = parse_macro_input!(item as Item);
106    let input = match input {
107        Item::Struct(item_struct) => item_struct,
108        other => return invalid_machine_target_error(&other).into(),
109    };
110    expand_machine(input).into()
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/// source-declared type alias that expands to that shape, or a supported
118/// wrapper around it, such as `Result<Machine<NextState>, E>`,
119/// `Option<Machine<NextState>>`, or
120/// `statum::Branch<Machine<Left>, Machine<Right>>`.
121///
122/// When the `strict-introspection` feature is enabled, transition graph
123/// semantics must be directly readable from the written return type or from a
124/// local `#[introspect(return = ...)]` escape hatch on the method.
125#[proc_macro_attribute]
126pub fn transition(
127    attr: proc_macro::TokenStream,
128    item: proc_macro::TokenStream,
129) -> proc_macro::TokenStream {
130    if !attr.is_empty() {
131        return syn::Error::new(
132            Span::call_site(),
133            DiagnosticMessage::new("`#[transition]` does not accept arguments.")
134                .found(format!("`#[transition({attr})]`"))
135                .expected("`#[transition] impl WorkflowMachine<Draft> { ... }`")
136                .fix("remove the attribute arguments and declare transition behavior with methods inside the impl block.".to_string())
137                .render(),
138        )
139        .to_compile_error()
140        .into();
141    }
142    let input = parse_macro_input!(item as ItemImpl);
143    expand_transition(input).into()
144}
145
146/// Rebuild typed machines from persisted data.
147///
148/// Apply `#[validators(Machine)]` or an anchored path such as
149/// `#[validators(self::path::Machine)]`,
150/// `#[validators(super::path::Machine)]`, or
151/// `#[validators(crate::path::Machine)]` to an `impl PersistedRow` block.
152/// Statum expects one `is_{state}` method per state variant and generates
153/// `into_machine()`, `.into_machines()`, and `.into_machines_by(...)` helpers
154/// for typed rehydration. Validator methods can return `Result<T, _>` for
155/// ordinary membership checks or `Validation<T>` when rebuild reports should
156/// carry stable rejection details through `.build_report()` and
157/// `.build_reports()`. In relaxed mode, bare multi-segment paths like
158/// `#[validators(flow::Machine)]` are treated as local child-module paths, not
159/// imported aliases or re-exports. If Statum cannot resolve that local path,
160/// it emits a compile error asking for an anchored path instead.
161#[proc_macro_attribute]
162pub fn validators(attr: TokenStream, item: TokenStream) -> TokenStream {
163    if attr.is_empty() {
164        return syn::Error::new(
165            Span::call_site(),
166            DiagnosticMessage::new("`#[validators(...)]` requires a machine path.")
167                .expected("`#[validators(WorkflowMachine)] impl PersistedRow { ... }`")
168                .fix("pass the target Statum machine type in the attribute, for example `#[validators(self::flow::WorkflowMachine)]`.".to_string())
169                .render(),
170        )
171        .to_compile_error()
172        .into();
173    }
174    let item_impl = parse_macro_input!(item as ItemImpl);
175    let module_path = match resolved_current_module_path(item_impl.self_ty.span(), "#[validators]")
176    {
177        Ok(path) => path,
178        Err(err) => return err,
179    };
180    parse_validators(attr, item_impl, &module_path)
181}
182
183pub(crate) fn resolved_current_module_path(
184    span: Span,
185    macro_name: &str,
186) -> Result<String, TokenStream> {
187    let resolved = module_path_for_span(span)
188        .or_else(current_module_path_opt)
189        .or_else(|| {
190            source_info_for_span(span)
191                .is_none()
192                .then_some("crate".to_string())
193        });
194
195    resolved.ok_or_else(|| {
196        let message = format!(
197            "Internal error: could not resolve the module path for `{macro_name}` at this call site."
198        );
199        quote::quote_spanned! { span =>
200            compile_error!(#message);
201        }
202        .into()
203    })
204}