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}