Skip to main content

obzenflow_fsm_macros/
lib.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2// SPDX-FileCopyrightText: 2025-2026 ObzenFlow Contributors
3// https://obzenflow.dev
4
5//! Proc-macro helpers for `obzenflow-fsm`.
6//!
7//! This crate is an implementation detail of `obzenflow-fsm`. End users should depend on
8//! `obzenflow-fsm` and use the re-exported macros from that crate:
9//! - `#[derive(obzenflow_fsm::StateVariant, obzenflow_fsm::EventVariant)]`
10//! - `obzenflow_fsm::fsm! { ... }`
11
12use proc_macro::TokenStream;
13use quote::quote;
14use syn::{parse_macro_input, DeriveInput};
15
16/// Derive `::obzenflow_fsm::StateVariant` for an enum.
17///
18/// The generated `variant_name()` matches on the enum and returns the variant identifier as a
19/// `&'static str`, ignoring any payload.
20#[proc_macro_derive(StateVariant)]
21pub fn derive_state_variant(input: TokenStream) -> TokenStream {
22    let input = parse_macro_input!(input as DeriveInput);
23    let enum_ident = input.ident;
24
25    let data_enum = match input.data {
26        syn::Data::Enum(e) => e,
27        _ => {
28            return syn::Error::new_spanned(
29                enum_ident,
30                "StateVariant can only be derived for enums",
31            )
32            .to_compile_error()
33            .into();
34        }
35    };
36
37    let arms = data_enum.variants.iter().map(|variant| {
38        let ident = &variant.ident;
39        match &variant.fields {
40            syn::Fields::Unit => quote! { Self::#ident => stringify!(#ident), },
41            syn::Fields::Unnamed(_) => quote! { Self::#ident(..) => stringify!(#ident), },
42            syn::Fields::Named(_) => quote! { Self::#ident { .. } => stringify!(#ident), },
43        }
44    });
45
46    let expanded = quote! {
47        impl ::obzenflow_fsm::StateVariant for #enum_ident {
48            fn variant_name(&self) -> &str {
49                match self {
50                    #( #arms )*
51                }
52            }
53        }
54    };
55
56    TokenStream::from(expanded)
57}
58
59/// Derive `::obzenflow_fsm::EventVariant` for an enum.
60///
61/// The generated `variant_name()` matches on the enum and returns the variant identifier as a
62/// `&'static str`, ignoring any payload.
63#[proc_macro_derive(EventVariant)]
64pub fn derive_event_variant(input: TokenStream) -> TokenStream {
65    let input = parse_macro_input!(input as DeriveInput);
66    let enum_ident = input.ident;
67
68    let data_enum = match input.data {
69        syn::Data::Enum(e) => e,
70        _ => {
71            return syn::Error::new_spanned(
72                enum_ident,
73                "EventVariant can only be derived for enums",
74            )
75            .to_compile_error()
76            .into();
77        }
78    };
79
80    let arms = data_enum.variants.iter().map(|variant| {
81        let ident = &variant.ident;
82        match &variant.fields {
83            syn::Fields::Unit => quote! { Self::#ident => stringify!(#ident), },
84            syn::Fields::Unnamed(_) => quote! { Self::#ident(..) => stringify!(#ident), },
85            syn::Fields::Named(_) => quote! { Self::#ident { .. } => stringify!(#ident), },
86        }
87    });
88
89    let expanded = quote! {
90        impl ::obzenflow_fsm::EventVariant for #enum_ident {
91            fn variant_name(&self) -> &str {
92                match self {
93                    #( #arms )*
94                }
95            }
96        }
97    };
98
99    TokenStream::from(expanded)
100}
101
102/// High-level typed FSM builder DSL.
103///
104/// This macro is re-exported as `obzenflow_fsm::fsm!` and is the recommended way to construct a
105/// `obzenflow_fsm::StateMachine`.
106///
107/// Supported syntax (first pass):
108/// - Top-level `state:`, `event:`, `context:`, `action:`, `initial:`.
109/// - Optional `unhandled => handler;` at top-level.
110/// - `state <State::Variant> { ... }` blocks containing:
111///   - `on <Event::Variant> => handler;`
112///   - `timeout <expr> => handler;`
113///   - `on_entry handler;`
114///   - `on_exit handler;`
115///
116/// Handler shapes:
117/// - `on` handlers take `(&State, &Event, &mut Context)` and return a `Transition`.
118/// - `timeout` handlers take `(&State, &mut Context)` and return a `Transition`.
119/// - `on_entry` / `on_exit` handlers take `(&State, &mut Context)` and return `Vec<Action>`.
120/// - `unhandled` handlers take `(&State, &Event, &mut Context)` and return `()`.
121///
122/// The macro currently expands to a legacy `FsmBuilder` pipeline ending in `.build()`.
123/// As a result, dispatch is performed by `StateVariant::variant_name()` and
124/// `EventVariant::variant_name()` (not by full type paths).
125///
126/// ```rust,ignore
127/// use obzenflow_fsm::{fsm, Transition};
128///
129/// #[derive(Clone, Debug, PartialEq, obzenflow_fsm::StateVariant)]
130/// enum State {
131///     Idle,
132/// }
133///
134/// #[derive(Clone, Debug, obzenflow_fsm::EventVariant)]
135/// enum Event {
136///     Start,
137/// }
138///
139/// #[derive(Clone, Debug, PartialEq)]
140/// enum Action {
141///     Noop,
142/// }
143///
144/// #[derive(Default)]
145/// struct Context;
146///
147/// impl obzenflow_fsm::FsmContext for Context {}
148///
149/// #[async_trait::async_trait]
150/// impl obzenflow_fsm::FsmAction for Action {
151///     type Context = Context;
152///
153///     async fn execute(&self, _ctx: &mut Self::Context) -> obzenflow_fsm::types::FsmResult<()> {
154///         Ok(())
155///     }
156/// }
157///
158/// let _machine = fsm! {
159///     state:   State;
160///     event:   Event;
161///     context: Context;
162///     action:  Action;
163///     initial: State::Idle;
164///
165///     unhandled => |_s: &State, _e: &Event, _ctx: &mut Context| {
166///         Box::pin(async move { Ok(()) })
167///     };
168///
169///     state State::Idle {
170///         on Event::Start => |_s: &State, _e: &Event, _ctx: &mut Context| {
171///             Box::pin(async move {
172///                 Ok(Transition {
173///                     next_state: State::Idle,
174///                     actions: vec![Action::Noop],
175///                 })
176///             })
177///         };
178///     }
179/// };
180/// ```
181#[proc_macro]
182pub fn fsm(input: TokenStream) -> TokenStream {
183    // Parse the incoming token stream into our simplified FSM spec,
184    // then expand it into a `FsmBuilder` chain that ends in `.build()`.
185    let input_ts = proc_macro2::TokenStream::from(input);
186
187    let parsed = match crate::parse::FsmSpec::parse(input_ts) {
188        Ok(spec) => spec,
189        Err(err) => return err.to_compile_error().into(),
190    };
191
192    let expanded = crate::codegen::expand_fsm_spec(&parsed);
193    TokenStream::from(expanded)
194}
195
196mod codegen;
197mod parse;