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;
48mod constructor_attr;
49mod constructor_provider;
50pub(crate) mod packaged_workflow;
51mod reactor_attr;
52mod registry;
53pub(crate) mod tasks;
54mod trigger_attr;
55mod workflow_attr;
56
57use proc_macro::TokenStream;
58
59/// Define a task with retry policies and trigger rules.
60#[proc_macro_attribute]
61pub fn task(args: TokenStream, input: TokenStream) -> TokenStream {
62    tasks::task(args, input)
63}
64
65/// Define a workflow as a module containing `#[task]` functions.
66///
67/// Applied to a `pub mod` containing `#[task]` functions. Auto-discovers tasks,
68/// validates dependencies, and generates registration code based on delivery mode:
69///
70/// - **Embedded** (default): `inventory::submit!` auto-registration consumed by
71///   `cloacina::Runtime::seed_from_inventory`
72/// - **Packaged** (`features = ["packaged"]`): FFI exports for `.cloacina` packages
73///
74/// # Example
75///
76/// ```rust,ignore
77/// #[workflow(name = "my_pipeline", description = "Process data")]
78/// pub mod my_pipeline {
79///     use super::*;
80///
81///     #[task(id = "fetch", dependencies = [])]
82///     pub async fn fetch(ctx: &mut Context<Value>) -> Result<(), TaskError> { Ok(()) }
83///
84///     #[task(id = "process", dependencies = ["fetch"])]
85///     pub async fn process(ctx: &mut Context<Value>) -> Result<(), TaskError> { Ok(()) }
86/// }
87/// ```
88#[proc_macro_attribute]
89pub fn workflow(args: TokenStream, input: TokenStream) -> TokenStream {
90    workflow_attr::workflow_attr(args, input)
91}
92
93/// Define a trigger that fires a workflow on a schedule or condition.
94///
95/// # Custom poll trigger
96///
97/// ```rust,ignore
98/// #[trigger(on = "my_workflow", poll_interval = "5s")]
99/// pub async fn check_inbox() -> Result<TriggerResult, TriggerError> {
100///     // check condition, return Fire(ctx) or Skip
101/// }
102/// ```
103///
104/// # Cron trigger (T-0305)
105///
106/// ```rust,ignore
107/// #[trigger(on = "my_workflow", cron = "0 2 * * *", timezone = "UTC")]
108/// ```
109#[proc_macro_attribute]
110pub fn trigger(args: TokenStream, input: TokenStream) -> TokenStream {
111    trigger_attr::trigger_attr(args, input)
112}
113
114/// Define a computation graph as a module containing async node functions.
115///
116/// The topology is declared in the macro attribute. Nodes are pure async functions
117/// within the module. The macro compiles the topology into a single async function
118/// with nested match arms for enum routing.
119///
120/// # Example
121///
122/// ```rust,ignore
123/// #[computation_graph(
124///     react = when_any(alpha, beta),
125///     graph = {
126///         decision(alpha, beta) => {
127///             Signal -> output_handler,
128///             NoAction -> audit_logger,
129///         },
130///     }
131/// )]
132/// mod my_strategy {
133///     async fn decision(alpha: Option<&AlphaData>, beta: Option<&BetaData>) -> DecisionOutcome { ... }
134///     async fn output_handler(signal: &Signal) -> OutputConfirmation { ... }
135///     async fn audit_logger(reason: &NoActionReason) -> AuditRecord { ... }
136/// }
137/// ```
138#[proc_macro_attribute]
139pub fn computation_graph(args: TokenStream, input: TokenStream) -> TokenStream {
140    computation_graph::computation_graph_attr(args, input)
141}
142
143/// Declare a reactor as a unit struct.
144///
145/// The reactor bundles accumulators + firing criteria and publishes an
146/// `InputCache` whenever its criteria are met. Graphs declared with
147/// `#[computation_graph(trigger = reactor(ReactorType), ...)]` bind to it
148/// by type path.
149///
150/// ```rust,ignore
151/// #[reactor(
152///     name = "risk_signals",
153///     accumulators = [alpha, beta],
154///     criteria = when_any(alpha, beta),
155/// )]
156/// pub struct RiskSignals;
157/// ```
158#[proc_macro_attribute]
159pub fn reactor(args: TokenStream, input: TokenStream) -> TokenStream {
160    reactor_attr::reactor_attr(args, input)
161}
162
163/// Author a WASM **constructor** member in clean form (CLOACI-T-0826 / T-0837).
164///
165/// A constructor is one MEMBER of a provider *suite*: the macro generates the
166/// member's config struct, its object-safe `<Kind>Object` impl (the `JSON`-wire
167/// body), and the associated metadata fns (`__constructor_name`,
168/// `__constructor_manifest`, `__constructor_make`) the crate-level
169/// [`macro@constructor_provider`] shell aggregates. The macro does NOT emit fidius
170/// `#[plugin_interface]` / `#[plugin_impl]` glue — that is the shell's job, so N
171/// constructors can coexist in one crate behind one component (CLOACI-A-0011).
172///
173/// Applied to a struct whose fields are `#[config]` (bound once per instance at
174/// load) or `#[param(required)]` / `#[param(optional)]` (declared inputs pulled
175/// from the task context — `task` kind only). The author writes ONLY the body
176/// method (`execute` for `kind = task`).
177///
178/// ```rust,ignore
179/// #[constructor(kind = task, name = "prefix", version = "0.1.0")]
180/// struct Prefix {
181///     #[config] prefix: String,
182///     #[param(required)] name: String,
183/// }
184/// impl Prefix {
185///     fn execute(&self) -> Result<(), ConstructorError> {
186///         self.set("result", format!("{}{}", self.prefix, self.name));
187///         Ok(())
188///     }
189/// }
190///
191/// // One provider = a suite of members behind one component:
192/// constructor_provider!(name = "cloacina-provider-text", version = "0.1.0", task = [Prefix]);
193/// ```
194///
195/// All four kinds are code-generated: `task` (T-0826), `trigger` (T-0829), and
196/// `accumulator` / `reactor` (T-0828). `trigger`'s author body returns a fire
197/// decision (`Result<bool, ConstructorError>`) and uses `set`/`get` to build the
198/// fired context.
199#[proc_macro_attribute]
200pub fn constructor(args: TokenStream, input: TokenStream) -> TokenStream {
201    constructor_attr::constructor_attr(args, input)
202}
203
204/// Declare a constructor **provider** — a *suite* of `#[constructor]` members
205/// compiled into one WASM component (CLOACI-A-0011 / T-0837).
206///
207/// Aggregates the explicitly-listed members (by struct name, grouped by kind) into
208/// the per-kind fidius `#[plugin_interface]` + `#[plugin_impl]` shell with a
209/// name-dispatched `configure` (the consumer's `constructor = "<name>"` selects
210/// the member; the name travels in the `configure` payload), and emits a crate-level
211/// `pub fn __provider_manifest() -> ProviderManifest` listing every member. A
212/// single-constructor provider is just a suite of one.
213///
214/// ```rust,ignore
215/// #[constructor(kind = task, name = "read_file", version = "0.1.0")]
216/// struct ReadFile { #[config] path: String }
217/// // ... impl ReadFile { fn execute(&self) -> Result<(), ConstructorError> { ... } }
218///
219/// #[constructor(kind = task, name = "write_file", version = "0.1.0")]
220/// struct WriteFile { #[config] path: String, #[param(required)] contents: String }
221/// // ... impl WriteFile { fn execute(&self) -> Result<(), ConstructorError> { ... } }
222///
223/// constructor_provider!(
224///     name = "cloacina-provider-fs",
225///     version = "0.1.0",
226///     task = [ReadFile, WriteFile],
227/// );
228/// ```
229#[proc_macro]
230pub fn constructor_provider(input: TokenStream) -> TokenStream {
231    constructor_provider::constructor_provider(input)
232}
233
234/// Define a passthrough accumulator (socket-only, no event loop).
235///
236/// ```rust,ignore
237/// #[passthrough_accumulator]
238/// fn beta(event: PricingUpdate) -> BetaData {
239///     BetaData { estimate: event.mid_price }
240/// }
241/// ```
242#[proc_macro_attribute]
243pub fn passthrough_accumulator(args: TokenStream, input: TokenStream) -> TokenStream {
244    match computation_graph::accumulator_macros::passthrough_accumulator_impl(
245        args.into(),
246        input.into(),
247    ) {
248        Ok(output) => output.into(),
249        Err(err) => err.to_compile_error().into(),
250    }
251}
252
253/// Define a stream-backed accumulator.
254///
255/// ```rust,ignore
256/// #[stream_accumulator(type = "kafka", topic = "market.orderbook")]
257/// fn alpha(event: OrderBookUpdate) -> AlphaData {
258///     AlphaData { top_high: event.best_ask, top_low: event.best_bid }
259/// }
260/// ```
261#[proc_macro_attribute]
262pub fn stream_accumulator(args: TokenStream, input: TokenStream) -> TokenStream {
263    match computation_graph::accumulator_macros::stream_accumulator_impl(args.into(), input.into())
264    {
265        Ok(output) => output.into(),
266        Err(err) => err.to_compile_error().into(),
267    }
268}
269
270/// Define a batch accumulator (buffers events, flushes on timer or size threshold).
271///
272/// ```rust,ignore
273/// #[batch_accumulator(flush_interval = "1s", max_buffer_size = 100)]
274/// fn aggregate_fills(events: Vec<FillEvent>) -> Option<AggregatedFills> {
275///     if events.is_empty() { return None; }
276///     Some(AggregatedFills { total: events.len(), volume: events.iter().map(|e| e.qty).sum() })
277/// }
278/// ```
279#[proc_macro_attribute]
280pub fn batch_accumulator(args: TokenStream, input: TokenStream) -> TokenStream {
281    match computation_graph::accumulator_macros::batch_accumulator_impl(args.into(), input.into()) {
282        Ok(output) => output.into(),
283        Err(err) => err.to_compile_error().into(),
284    }
285}
286
287/// Define a polling accumulator (timer-based, queries pull-based sources).
288///
289/// ```rust,ignore
290/// #[polling_accumulator(interval = "5s")]
291/// async fn config_source() -> Option<ConfigData> {
292///     let data = fetch_config().await;
293///     if data.changed() { Some(data) } else { None }
294/// }
295/// ```
296#[proc_macro_attribute]
297pub fn polling_accumulator(args: TokenStream, input: TokenStream) -> TokenStream {
298    match computation_graph::accumulator_macros::polling_accumulator_impl(args.into(), input.into())
299    {
300        Ok(output) => output.into(),
301        Err(err) => err.to_compile_error().into(),
302    }
303}
304
305/// Define a state accumulator (bounded history buffer with DAL persistence).
306///
307/// ```rust,ignore
308/// #[state_accumulator(capacity = 10)]
309/// fn previous_outputs() -> VecDeque<DecisionOutput>;
310/// ```
311#[proc_macro_attribute]
312pub fn state_accumulator(args: TokenStream, input: TokenStream) -> TokenStream {
313    match computation_graph::accumulator_macros::state_accumulator_impl(args.into(), input.into()) {
314        Ok(output) => output.into(),
315        Err(err) => err.to_compile_error().into(),
316    }
317}