Skip to main content

temporalio_macros/
lib.rs

1use proc_macro::TokenStream;
2use proc_macro2::TokenStream as TokenStream2;
3use syn::{parse::Parser, parse_macro_input};
4
5mod activities_definitions;
6mod fsm_impl;
7mod macro_utils;
8mod workflow_definitions;
9
10/// Can be used to define Activities for invocation and execution. Using this macro requires that
11/// you also depend on the `temporalio_sdk` crate.
12///
13/// For a usage example, see that crate's documentation.
14#[proc_macro_attribute]
15pub fn activities(_attr: TokenStream, item: TokenStream) -> TokenStream {
16    let def = parse_macro_input!(item with activities_definitions::parse_activities);
17    def.codegen()
18}
19
20/// Marks a method within an `#[activities]` impl block as an activity.
21/// This attribute is processed by the `#[activities]` macro and should not be used standalone.
22#[proc_macro_attribute]
23pub fn activity(_attr: TokenStream, item: TokenStream) -> TokenStream {
24    item
25}
26
27/// Declares activities without providing implementations. Each method must omit the
28/// `ActivityContext` parameter and have a body of exactly `unimplemented!()`.
29///
30/// Intended for workflow crates that need typed activity declarations which are implemented
31/// elsewhere by a separate worker crate (or in another language).
32#[proc_macro_attribute]
33pub fn activity_definitions(_attr: TokenStream, item: TokenStream) -> TokenStream {
34    let def = parse_macro_input!(item with activities_definitions::parse_definitions);
35    def.codegen()
36}
37
38/// Marks a struct as a workflow definition.
39///
40/// By default, the struct name is used as the workflow type name. To specify a custom workflow
41/// name, use `#[run(name = "my-custom-workflow")]` on the run method.
42///
43/// This attribute must be used in conjunction with `#[workflow_methods]` on an impl block.
44#[proc_macro_attribute]
45pub fn workflow(attr: TokenStream, item: TokenStream) -> TokenStream {
46    match validate_workflow_attributes(attr.into()) {
47        Ok(()) => item,
48        Err(err) => err.into_compile_error().into(),
49    }
50}
51
52fn validate_workflow_attributes(attr: TokenStream2) -> syn::Result<()> {
53    let parser = syn::meta::parser(|meta| {
54        if meta.path.is_ident("name") {
55            Err(meta.error("`name` is not supported on #[workflow]; use #[run(name = ...)] on the workflow run method"))
56        } else {
57            Err(meta.error("unsupported workflow attribute"))
58        }
59    });
60    parser.parse2(attr)
61}
62
63/// Defines workflow methods for a workflow struct. Using this macro requires that
64/// you also depend on the `temporalio_sdk` crate.
65///
66/// This macro processes an impl block and generates:
67/// - Marker structs for each workflow method
68/// - Trait implementations for workflow definition and execution
69/// - Registration code for workers
70///
71/// ## Macro Attributes
72///
73/// - `factory_only` - When set, the workflow must be registered using
74///   `register_workflow_with_factory` and does not need to implement `Default` or define an `#[init]`
75///   method. Ex: `#[workflow_methods(factory_only)]`
76///
77/// ## Method Attributes
78///
79/// - `#[init]` - Optional initialization method. Signature: `fn new(input: T, ctx: &WorkflowContext) -> Self`
80/// - `#[run]` - Required main workflow function. Signature: `async fn run(&mut self, ctx: &mut WorkflowContext) -> WorkflowResult<T>`. Supports optional `name`.
81/// - `#[signal]` - Signal handler. Sync: `fn signal(&mut self, ctx: &mut SyncWorkflowContext, input: T)`. Async: `async fn signal(ctx: &mut WorkflowContext, input: T)`
82/// - `#[query]` - Query handler. Signature: `fn query(&self, ctx: &WorkflowContextView, input: T) -> R` (must NOT be async)
83/// - `#[update]` - Update handler. Sync: `fn update(&mut self, ctx: &mut SyncWorkflowContext, input: T) -> R`. Async: `async fn update(ctx: &mut WorkflowContext, input: T) -> R`
84///
85/// For a usage example, see the `temporalio_sdk` crate's documentation.
86#[proc_macro_attribute]
87pub fn workflow_methods(attr: TokenStream, item: TokenStream) -> TokenStream {
88    let factory_only = !attr.is_empty() && attr.to_string().contains("factory_only");
89    let def: workflow_definitions::WorkflowMethodsDefinition =
90        parse_macro_input!(item as workflow_definitions::WorkflowMethodsDefinition);
91    def.codegen_with_options(factory_only)
92}
93
94/// Marks a method within a `#[workflow_methods]` impl block as the initialization method.
95/// This attribute is processed by the `#[workflow_methods]` macro and should not be used standalone.
96#[proc_macro_attribute]
97pub fn init(_attr: TokenStream, item: TokenStream) -> TokenStream {
98    item
99}
100
101/// Marks a method within a `#[workflow_methods]` impl block as the main run method.
102/// This attribute is processed by the `#[workflow_methods]` macro and should not be used standalone.
103#[proc_macro_attribute]
104pub fn run(_attr: TokenStream, item: TokenStream) -> TokenStream {
105    item
106}
107
108/// Marks a method within a `#[workflow_methods]` impl block as a signal handler.
109/// This attribute is processed by the `#[workflow_methods]` macro and should not be used standalone.
110///
111/// Supports an optional `name` parameter to override the signal name:
112/// `#[signal(name = "my_signal")]`
113#[proc_macro_attribute]
114pub fn signal(_attr: TokenStream, item: TokenStream) -> TokenStream {
115    item
116}
117
118/// Marks a method within a `#[workflow_methods]` impl block as a query handler.
119/// This attribute is processed by the `#[workflow_methods]` macro and should not be used standalone.
120///
121/// Supports an optional `name` parameter to override the query name:
122/// `#[query(name = "my_query")]`
123#[proc_macro_attribute]
124pub fn query(_attr: TokenStream, item: TokenStream) -> TokenStream {
125    item
126}
127
128/// Marks a method within a `#[workflow_methods]` impl block as an update handler.
129/// This attribute is processed by the `#[workflow_methods]` macro and should not be used standalone.
130///
131/// Supports an optional `name` parameter to override the update name:
132/// `#[update(name = "my_update")]`
133#[proc_macro_attribute]
134pub fn update(_attr: TokenStream, item: TokenStream) -> TokenStream {
135    item
136}
137
138/// Marks a method within a `#[workflow_methods]` impl block as a validator for an update handler.
139/// This attribute is processed by the `#[workflow_methods]` macro and should not be used standalone.
140///
141/// The parameter specifies which update this validator applies to:
142/// `#[update_validator(my_update)]`
143///
144/// The validator method must:
145/// - Take `&self` (not `&mut self`)
146/// - Take `&WorkflowContextView` as the first parameter
147/// - Take a reference to the update's input type as the second parameter
148/// - Return `Result<(), Box<dyn std::error::Error + Send + Sync>>`
149#[proc_macro_attribute]
150pub fn update_validator(_attr: TokenStream, item: TokenStream) -> TokenStream {
151    item
152}
153
154/// Parses a DSL for defining finite state machines, and produces code implementing the
155/// [StateMachine](trait.StateMachine.html) trait.
156///
157/// An example state machine definition of a card reader for unlocking a door:
158/// ```
159/// use std::convert::Infallible;
160/// use temporalio_common::fsm_trait::{StateMachine, TransitionResult};
161/// use temporalio_macros::fsm;
162///
163/// fsm! {
164///     name CardReader; command Commands; error Infallible; shared_state SharedState;
165///
166///     Locked --(CardReadable(CardData), shared on_card_readable) --> ReadingCard;
167///     Locked --(CardReadable(CardData), shared on_card_readable) --> Locked;
168///     ReadingCard --(CardAccepted, on_card_accepted) --> DoorOpen;
169///     ReadingCard --(CardRejected, on_card_rejected) --> Locked;
170///     DoorOpen --(DoorClosed, on_door_closed) --> Locked;
171/// }
172///
173/// #[derive(Clone)]
174/// pub struct SharedState {
175///     last_id: Option<String>,
176/// }
177///
178/// #[derive(Debug, Clone, Eq, PartialEq, Hash)]
179/// pub enum Commands {
180///     StartBlinkingLight,
181///     StopBlinkingLight,
182///     ProcessData(CardData),
183/// }
184///
185/// type CardData = String;
186///
187/// /// Door is locked / idle / we are ready to read
188/// #[derive(Debug, Clone, Eq, PartialEq, Hash, Default)]
189/// pub struct Locked {}
190///
191/// /// Actively reading the card
192/// #[derive(Debug, Clone, Eq, PartialEq, Hash)]
193/// pub struct ReadingCard {
194///     card_data: CardData,
195/// }
196///
197/// /// The door is open, we shouldn't be accepting cards and should be blinking the light
198/// #[derive(Debug, Clone, Eq, PartialEq, Hash)]
199/// pub struct DoorOpen {}
200/// impl DoorOpen {
201///     fn on_door_closed(&self) -> CardReaderTransition<Locked> {
202///         TransitionResult::ok(vec![], Locked {})
203///     }
204/// }
205///
206/// impl Locked {
207///     fn on_card_readable(
208///         &self,
209///         shared_dat: &mut SharedState,
210///         data: CardData,
211///     ) -> CardReaderTransition<ReadingCardOrLocked> {
212///         match &shared_dat.last_id {
213///             // Arbitrarily deny the same person entering twice in a row
214///             Some(d) if d == &data => TransitionResult::ok(vec![], Locked {}.into()),
215///             _ => {
216///                 // Otherwise issue a processing command. This illustrates using the same handler
217///                 // for different destinations
218///                 shared_dat.last_id = Some(data.clone());
219///                 TransitionResult::ok(
220///                     vec![
221///                         Commands::ProcessData(data.clone()),
222///                         Commands::StartBlinkingLight,
223///                     ],
224///                     ReadingCard { card_data: data }.into(),
225///                 )
226///             }
227///         }
228///     }
229/// }
230///
231/// impl ReadingCard {
232///     fn on_card_accepted(&self) -> CardReaderTransition<DoorOpen> {
233///         TransitionResult::ok(vec![Commands::StopBlinkingLight], DoorOpen {})
234///     }
235///     fn on_card_rejected(&self) -> CardReaderTransition<Locked> {
236///         TransitionResult::ok(vec![Commands::StopBlinkingLight], Locked {})
237///     }
238/// }
239///
240/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
241/// let crs = CardReaderState::Locked(Locked {});
242/// let mut cr = CardReader::from_parts(crs, SharedState { last_id: None });
243/// let cmds = cr.on_event(CardReaderEvents::CardReadable("badguy".to_string()))?;
244/// assert_eq!(cmds[0], Commands::ProcessData("badguy".to_string()));
245/// assert_eq!(cmds[1], Commands::StartBlinkingLight);
246///
247/// let cmds = cr.on_event(CardReaderEvents::CardRejected)?;
248/// assert_eq!(cmds[0], Commands::StopBlinkingLight);
249///
250/// let cmds = cr.on_event(CardReaderEvents::CardReadable("goodguy".to_string()))?;
251/// assert_eq!(cmds[0], Commands::ProcessData("goodguy".to_string()));
252/// assert_eq!(cmds[1], Commands::StartBlinkingLight);
253///
254/// let cmds = cr.on_event(CardReaderEvents::CardAccepted)?;
255/// assert_eq!(cmds[0], Commands::StopBlinkingLight);
256/// # Ok(())
257/// # }
258/// ```
259///
260/// In the above example the first word is the name of the state machine, then after the comma the
261/// type (which you must define separately) of commands produced by the machine.
262///
263/// then each line represents a transition, where the first word is the initial state, the tuple
264/// inside the arrow is `(eventtype[, event handler])`, and the word after the arrow is the
265/// destination state. here `eventtype` is an enum variant , and `event_handler` is a function you
266/// must define outside the enum whose form depends on the event variant. the only variant types
267/// allowed are unit and one-item tuple variants. For unit variants, the function takes no
268/// parameters. For the tuple variants, the function takes the variant data as its parameter. In
269/// either case the function is expected to return a `TransitionResult` to the appropriate state.
270///
271/// The first transition can be interpreted as "If the machine is in the locked state, when a
272/// `CardReadable` event is seen, call `on_card_readable` (passing in `CardData`) and transition to
273/// the `ReadingCard` state.
274///
275/// The macro will generate a few things:
276/// * A struct for the overall state machine, named with the provided name. Here:
277///   ```text
278///   struct CardReader {
279///       state: CardReaderState,
280///       shared_state: SharedState,
281///   }
282///   ```
283/// * An enum with a variant for each state, named with the provided name + "State".
284///   ```text
285///   enum CardReaderState {
286///       Locked(Locked),
287///       ReadingCard(ReadingCard),
288///       DoorOpen(DoorOpen),
289///   }
290///   ```
291///
292///   You are expected to define a type for each state, to contain that state's data. If there is
293///   no data, you can simply: `type StateName = ()`
294/// * For any instance of transitions with the same event/handler which transition to different
295///   destination states (dynamic destinations), an enum named like `DestAOrDestBOrDestC` is
296///   generated. This enum must be used as the destination "state" from those handlers.
297/// * An enum with a variant for each event. You are expected to define the type (if any) contained
298///   in the event variant.
299///   ```text
300///   enum CardReaderEvents {
301///     DoorClosed,
302///     CardAccepted,
303///     CardRejected,
304///     CardReadable(CardData),
305///   }
306///   ```
307/// * An implementation of the [StateMachine](trait.StateMachine.html) trait for the generated state
308///   machine enum (in this case, `CardReader`)
309/// * A type alias for a [TransitionResult](enum.TransitionResult.html) with the appropriate generic
310///   parameters set for your machine. It is named as your machine with `Transition` appended. In
311///   this case, `CardReaderTransition`.
312#[proc_macro]
313pub fn fsm(input: TokenStream) -> TokenStream {
314    let def: fsm_impl::StateMachineDefinition =
315        parse_macro_input!(input as fsm_impl::StateMachineDefinition);
316    def.codegen()
317}
318
319#[cfg(test)]
320mod tests {
321    use super::validate_workflow_attributes;
322    use quote::quote;
323
324    #[test]
325    fn workflow_attribute_rejects_name_override() {
326        let err = validate_workflow_attributes(quote!(name = "RenamedWorkflow")).unwrap_err();
327        assert!(
328            err.to_string()
329                .contains("use #[run(name = ...)] on the workflow run method")
330        );
331    }
332}