Skip to main content

cloacina_macros/
lib.rs

1/*
2 *  Copyright 2025-2026 Colliery Software
3 *
4 *  Licensed under the Apache License, Version 2.0 (the "License");
5 *  you may not use this file except in compliance with the License.
6 *  You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 *  Unless required by applicable law or agreed to in writing, software
11 *  distributed under the License is distributed on an "AS IS" BASIS,
12 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 *  See the License for the specific language governing permissions and
14 *  limitations under the License.
15 */
16
17#![allow(unexpected_cfgs)]
18//! # Cloacina Macros
19//!
20//! Procedural macros for defining tasks and workflows in the Cloacina framework.
21//!
22//! ## Key Features
23//!
24//! - `#[task]` — define tasks with retry policies and trigger rules
25//! - `#[workflow]` — define workflows as modules containing `#[task]` functions
26//! - Compile-time validation of task dependencies and workflow structure
27//! - Automatic task and workflow registration
28//! - Code fingerprinting for task versioning
29//!
30//! ## Example
31//!
32//! ```rust,ignore
33//! use cloacina::{task, workflow, Context, TaskError};
34//!
35//! #[workflow(name = "my_pipeline", description = "Process data")]
36//! pub mod my_pipeline {
37//!     use super::*;
38//!
39//!     #[task(id = "fetch", dependencies = [])]
40//!     pub async fn fetch(ctx: &mut Context<Value>) -> Result<(), TaskError> { Ok(()) }
41//!
42//!     #[task(id = "process", dependencies = ["fetch"])]
43//!     pub async fn process(ctx: &mut Context<Value>) -> Result<(), TaskError> { Ok(()) }
44//! }
45//! ```
46
47pub(crate) mod computation_graph;
48pub(crate) mod packaged_workflow;
49mod reactor_attr;
50mod registry;
51pub(crate) mod tasks;
52mod trigger_attr;
53mod workflow_attr;
54
55use proc_macro::TokenStream;
56
57/// Define a task with retry policies and trigger rules.
58#[proc_macro_attribute]
59pub fn task(args: TokenStream, input: TokenStream) -> TokenStream {
60    tasks::task(args, input)
61}
62
63/// Define a workflow as a module containing `#[task]` functions.
64///
65/// Applied to a `pub mod` containing `#[task]` functions. Auto-discovers tasks,
66/// validates dependencies, and generates registration code based on delivery mode:
67///
68/// - **Embedded** (default): `inventory::submit!` auto-registration consumed by
69///   `cloacina::Runtime::seed_from_inventory`
70/// - **Packaged** (`features = ["packaged"]`): FFI exports for `.cloacina` packages
71///
72/// # Example
73///
74/// ```rust,ignore
75/// #[workflow(name = "my_pipeline", description = "Process data")]
76/// pub mod my_pipeline {
77///     use super::*;
78///
79///     #[task(id = "fetch", dependencies = [])]
80///     pub async fn fetch(ctx: &mut Context<Value>) -> Result<(), TaskError> { Ok(()) }
81///
82///     #[task(id = "process", dependencies = ["fetch"])]
83///     pub async fn process(ctx: &mut Context<Value>) -> Result<(), TaskError> { Ok(()) }
84/// }
85/// ```
86#[proc_macro_attribute]
87pub fn workflow(args: TokenStream, input: TokenStream) -> TokenStream {
88    workflow_attr::workflow_attr(args, input)
89}
90
91/// Define a trigger that fires a workflow on a schedule or condition.
92///
93/// # Custom poll trigger
94///
95/// ```rust,ignore
96/// #[trigger(on = "my_workflow", poll_interval = "5s")]
97/// pub async fn check_inbox() -> Result<TriggerResult, TriggerError> {
98///     // check condition, return Fire(ctx) or Skip
99/// }
100/// ```
101///
102/// # Cron trigger (T-0305)
103///
104/// ```rust,ignore
105/// #[trigger(on = "my_workflow", cron = "0 2 * * *", timezone = "UTC")]
106/// ```
107#[proc_macro_attribute]
108pub fn trigger(args: TokenStream, input: TokenStream) -> TokenStream {
109    trigger_attr::trigger_attr(args, input)
110}
111
112/// Define a computation graph as a module containing async node functions.
113///
114/// The topology is declared in the macro attribute. Nodes are pure async functions
115/// within the module. The macro compiles the topology into a single async function
116/// with nested match arms for enum routing.
117///
118/// # Example
119///
120/// ```rust,ignore
121/// #[computation_graph(
122///     react = when_any(alpha, beta),
123///     graph = {
124///         decision(alpha, beta) => {
125///             Signal -> output_handler,
126///             NoAction -> audit_logger,
127///         },
128///     }
129/// )]
130/// mod my_strategy {
131///     async fn decision(alpha: Option<&AlphaData>, beta: Option<&BetaData>) -> DecisionOutcome { ... }
132///     async fn output_handler(signal: &Signal) -> OutputConfirmation { ... }
133///     async fn audit_logger(reason: &NoActionReason) -> AuditRecord { ... }
134/// }
135/// ```
136#[proc_macro_attribute]
137pub fn computation_graph(args: TokenStream, input: TokenStream) -> TokenStream {
138    computation_graph::computation_graph_attr(args, input)
139}
140
141/// Declare a reactor as a unit struct.
142///
143/// The reactor bundles accumulators + firing criteria and publishes an
144/// `InputCache` whenever its criteria are met. Graphs declared with
145/// `#[computation_graph(trigger = reactor(ReactorType), ...)]` bind to it
146/// by type path.
147///
148/// ```rust,ignore
149/// #[reactor(
150///     name = "risk_signals",
151///     accumulators = [alpha, beta],
152///     criteria = when_any(alpha, beta),
153/// )]
154/// pub struct RiskSignals;
155/// ```
156#[proc_macro_attribute]
157pub fn reactor(args: TokenStream, input: TokenStream) -> TokenStream {
158    reactor_attr::reactor_attr(args, input)
159}
160
161/// Define a passthrough accumulator (socket-only, no event loop).
162///
163/// ```rust,ignore
164/// #[passthrough_accumulator]
165/// fn beta(event: PricingUpdate) -> BetaData {
166///     BetaData { estimate: event.mid_price }
167/// }
168/// ```
169#[proc_macro_attribute]
170pub fn passthrough_accumulator(args: TokenStream, input: TokenStream) -> TokenStream {
171    match computation_graph::accumulator_macros::passthrough_accumulator_impl(
172        args.into(),
173        input.into(),
174    ) {
175        Ok(output) => output.into(),
176        Err(err) => err.to_compile_error().into(),
177    }
178}
179
180/// Define a stream-backed accumulator.
181///
182/// ```rust,ignore
183/// #[stream_accumulator(type = "kafka", topic = "market.orderbook")]
184/// fn alpha(event: OrderBookUpdate) -> AlphaData {
185///     AlphaData { top_high: event.best_ask, top_low: event.best_bid }
186/// }
187/// ```
188#[proc_macro_attribute]
189pub fn stream_accumulator(args: TokenStream, input: TokenStream) -> TokenStream {
190    match computation_graph::accumulator_macros::stream_accumulator_impl(args.into(), input.into())
191    {
192        Ok(output) => output.into(),
193        Err(err) => err.to_compile_error().into(),
194    }
195}
196
197/// Define a batch accumulator (buffers events, flushes on timer or size threshold).
198///
199/// ```rust,ignore
200/// #[batch_accumulator(flush_interval = "1s", max_buffer_size = 100)]
201/// fn aggregate_fills(events: Vec<FillEvent>) -> Option<AggregatedFills> {
202///     if events.is_empty() { return None; }
203///     Some(AggregatedFills { total: events.len(), volume: events.iter().map(|e| e.qty).sum() })
204/// }
205/// ```
206#[proc_macro_attribute]
207pub fn batch_accumulator(args: TokenStream, input: TokenStream) -> TokenStream {
208    match computation_graph::accumulator_macros::batch_accumulator_impl(args.into(), input.into()) {
209        Ok(output) => output.into(),
210        Err(err) => err.to_compile_error().into(),
211    }
212}
213
214/// Define a polling accumulator (timer-based, queries pull-based sources).
215///
216/// ```rust,ignore
217/// #[polling_accumulator(interval = "5s")]
218/// async fn config_source() -> Option<ConfigData> {
219///     let data = fetch_config().await;
220///     if data.changed() { Some(data) } else { None }
221/// }
222/// ```
223#[proc_macro_attribute]
224pub fn polling_accumulator(args: TokenStream, input: TokenStream) -> TokenStream {
225    match computation_graph::accumulator_macros::polling_accumulator_impl(args.into(), input.into())
226    {
227        Ok(output) => output.into(),
228        Err(err) => err.to_compile_error().into(),
229    }
230}
231
232/// Define a state accumulator (bounded history buffer with DAL persistence).
233///
234/// ```rust,ignore
235/// #[state_accumulator(capacity = 10)]
236/// fn previous_outputs() -> VecDeque<DecisionOutput>;
237/// ```
238#[proc_macro_attribute]
239pub fn state_accumulator(args: TokenStream, input: TokenStream) -> TokenStream {
240    match computation_graph::accumulator_macros::state_accumulator_impl(args.into(), input.into()) {
241        Ok(output) => output.into(),
242        Err(err) => err.to_compile_error().into(),
243    }
244}