Skip to main content

sayiir_macros/
lib.rs

1#![deny(missing_docs)]
2//! Procedural macros for the Sayiir durable workflow engine.
3//!
4//! Provides two macros that eliminate boilerplate when defining workflows:
5//!
6//! - **`#[task]`** — Transforms an async function into a `CoreTask` struct with
7//!   automatic registration, metadata, and dependency injection.
8//!
9//! - **`workflow!`** — Builds a workflow pipeline with a concise DSL that desugars
10//!   to `WorkflowBuilder` method calls.
11//!
12//! # Quick Example
13//!
14//! ```rust,ignore
15//! use sayiir_macros::task;
16//!
17//! #[task(timeout = "30s", retries = 3, backoff = "100ms")]
18//! async fn charge(order: Order, #[inject] stripe: Arc<Stripe>) -> Result<Receipt, BoxError> {
19//!     stripe.charge(&order).await
20//! }
21//!
22//! let workflow = workflow! {
23//!     name: "order-process",
24//!     codec: JsonCodec,
25//!     steps: [
26//!         validate(order: Order) { validate_order(order) },
27//!         charge,
28//!         (send_email || update_inventory),
29//!         finalize,
30//!     ]
31//! };
32//! ```
33
34mod branch_key;
35mod task;
36mod util;
37mod workflow;
38
39/// Transforms an async function into a `CoreTask` implementation.
40///
41/// # Attributes
42///
43/// - `id = "stable_name"` — **strongly recommended**: set an explicit, stable task ID.
44///   The default (function name) ties your workflow identity to code structure — renaming
45///   the function silently changes the ID, breaking in-flight workflows on resume.
46///   Always set `id` in production workflows.
47/// - `display_name = "Charge Card"` — human-readable name
48/// - `description = "Charges the customer's card"` — task description
49/// - `timeout = "30s"` — task timeout (supports `ms`, `s`, `m`, `h` suffixes)
50/// - `retries = 3` — maximum retry count
51/// - `backoff = "100ms"` — initial retry delay
52/// - `backoff_multiplier = 2.0` — exponential multiplier (default: 2.0)
53/// - `tags = "io"` — categorization tags (can be repeated)
54///
55/// # Parameters
56///
57/// - Exactly **one** non-`#[inject]` parameter: the task input type
58/// - Zero or more `#[inject]` parameters: dependency-injected fields
59///
60/// # Return Types
61///
62/// - `Result<T, E>` — fallible; `E` is converted via `Into<BoxError>`
63/// - `T` — infallible; automatically wrapped in `Ok(...)`
64///
65/// # Generated Code
66///
67/// - A PascalCase struct with `Task` suffix (e.g., `fn charge` → `struct ChargeTask`)
68/// - `new()` constructor with positional args for injected dependencies
69/// - `task_id()` — returns the task ID (explicit `id` or function name)
70/// - `metadata()` — returns `TaskMetadata` built from attributes
71/// - `register()` method for `TaskRegistry` integration
72/// - `CoreTask` trait implementation
73/// - The original function is preserved for direct use/testing
74///
75/// # Example
76///
77/// ```rust,ignore
78/// #[task(id = "charge_card", timeout = "30s", retries = 3)]
79/// async fn charge(order: Order, #[inject] stripe: Arc<Stripe>) -> Result<Receipt, BoxError> {
80///     stripe.charge(&order).await
81/// }
82///
83/// // Generated: ChargeTask with new(stripe), task_id() → "charge_card", etc.
84/// let task = ChargeTask::new(stripe);
85/// ChargeTask::register(&mut registry, codec, task);
86/// ```
87#[proc_macro_attribute]
88pub fn task(
89    attr: proc_macro::TokenStream,
90    item: proc_macro::TokenStream,
91) -> proc_macro::TokenStream {
92    match task::expand(attr.into(), item.into()) {
93        Ok(tokens) => tokens.into(),
94        Err(e) => e.to_compile_error().into(),
95    }
96}
97
98/// Derives `BranchKey` for a fieldless enum.
99///
100/// Each variant maps to a `snake_case` string key by default. Use
101/// `#[branch_key("custom_key")]` on a variant to override.
102///
103/// # Example
104///
105/// ```rust,ignore
106/// use sayiir_macros::BranchKey;
107///
108/// #[derive(BranchKey)]
109/// enum Intent {
110///     Billing,           // key = "billing"
111///     TechSupport,       // key = "tech_support"
112///     #[branch_key("other")]
113///     Fallback,          // key = "other"
114/// }
115/// ```
116#[proc_macro_derive(BranchKey, attributes(branch_key))]
117pub fn derive_branch_key(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
118    let input = syn::parse_macro_input!(input as syn::DeriveInput);
119    match branch_key::expand(input) {
120        Ok(tokens) => tokens.into(),
121        Err(e) => e.to_compile_error().into(),
122    }
123}
124
125/// Builds a workflow pipeline with a concise DSL.
126///
127/// # Syntax
128///
129/// ```text
130/// workflow! {
131///     name: "workflow-id",
132///     codec: CodecType,
133///     registry: registry_expr,  // optional — defaults to TaskRegistry::new()
134///     steps: [step, step, step]
135/// }
136/// ```
137///
138/// # Step Types
139///
140/// - `task_name` — reference to a `#[task]`-generated struct (resolved as `TaskNameTask`)
141/// - `name(param: Type) { expr }` — inline task
142/// - `(step || step), join` — parallel fork
143/// - `delay "5s"` — durable delay (auto-generated ID)
144/// - `delay "wait_24h" "5s"` — durable delay with custom ID
145/// - `signal "name"` — wait for external signal
146/// - `signal "name" timeout "30s"` — signal with timeout
147/// - `loop task_name N` — loop body task up to N iterations (default: fail on max)
148/// - `loop task_name N exit_with_last` — loop with exit-on-max policy
149/// - `flow expr` — inline a child workflow (merges its task registry)
150/// - `route key_fn { "k" => [steps], _ => [steps] }` — conditional routing (string keys)
151/// - `route key_fn -> Enum { Variant => [steps], _ => [steps] }` — typed conditional routing
152///
153/// # Returns
154///
155/// A `Result<SerializableWorkflow<C, Input, ()>, WorkflowError>` expression.
156#[proc_macro]
157pub fn workflow(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
158    match workflow::expand(input.into()) {
159        Ok(tokens) => tokens.into(),
160        Err(e) => e.to_compile_error().into(),
161    }
162}