cano_macros/lib.rs
1//! # cano-macros
2//!
3//! Procedural macros backing the [`cano`](https://docs.rs/cano) workflow engine.
4//!
5//! The crate exposes one attribute macro per `cano` core trait, all of which
6//! perform the same async-fn-to-`Pin<Box<dyn Future + Send>>` rewrite that the
7//! `async-trait` crate does. Splitting the macro by trait name (rather than a
8//! single generic `async_trait`) gives a more self-documenting attribute at the
9//! developer side: `#[cano::task]` on `impl Task` is immediately scannable for
10//! what the impl is.
11//!
12//! These macros are intended to be used via their namespaced paths in `cano`:
13//!
14//! - `#[cano::task]` — for `impl Task` (and the `Task` trait definition itself)
15//! - `#[cano::task::router]` — for `impl RouterTask` and the `RouterTask` trait
16//! - `#[cano::task::poll]` — for `impl PollTask` and the `PollTask` trait
17//! - `#[cano::task::timer]` — for `impl TimerTask` and the `TimerTask` trait
18//! - `#[cano::task::batch]` — for `impl BatchTask` and the `BatchTask` trait
19//! - `#[cano::task::stepped]` — for `impl SteppedTask` and the `SteppedTask` trait
20//! - `#[cano::task::stream]` — for `impl StreamTask` and the `StreamTask` trait
21//! - `#[cano::saga::task]` — for `impl CompensatableTask`
22//! - `#[cano::resource]` — for `impl Resource` and the `Resource` trait
23//! - `#[cano::checkpoint_store]` — for `impl CheckpointStore` and the `CheckpointStore` trait
24//!
25//! All are functionally identical in the async-rewrite they perform; they differ
26//! only in name. New traits that need async-fn-in-dyn rewriting can ship their
27//! own `cano-macros` attribute alongside.
28
29use proc_macro::TokenStream;
30
31mod async_rewrite;
32mod attr_args;
33mod batch_task_impl;
34mod checkpoint_store_impl;
35mod compensatable_task_impl;
36mod from_resources;
37mod path_prefix;
38mod poll_task_impl;
39mod resource_derive;
40mod router_task_impl;
41mod stepped_task_impl;
42mod stream_task_impl;
43mod task_impl;
44mod timer_task_impl;
45
46/// Derive a `from_resources(&Resources<_>) -> CanoResult<Self>` constructor that
47/// pulls each field out of a `cano::Resources` map.
48///
49/// Each field must be `Arc<T>`. Use `#[res("key")]` for string-literal lookups
50/// or `#[res(EnumType::Variant)]` for enum-path lookups. Use
51/// `#[from_resources(key = MyType)]` on the struct to override the inferred key type.
52///
53/// # Example
54///
55/// ```ignore
56/// use cano::prelude::*;
57/// use std::sync::Arc;
58///
59/// #[derive(FromResources)]
60/// struct Deps {
61/// #[res("store")]
62/// store: Arc<MemoryStore>,
63/// }
64/// ```
65#[proc_macro_derive(FromResources, attributes(res, from_resources))]
66pub fn derive_from_resources(input: TokenStream) -> TokenStream {
67 from_resources::expand(input.into())
68 .unwrap_or_else(syn::Error::into_compile_error)
69 .into()
70}
71
72/// Apply to `impl Task for ...` blocks (or the `Task` trait definition itself).
73///
74/// Rewrites every `async fn` method into a method returning
75/// `Pin<Box<dyn Future<Output = ...> + Send + 'async_trait>>`, the same shape
76/// `async-trait` produces. This makes the methods callable through
77/// `dyn Task<...>`.
78///
79/// Use as `#[cano::task]`.
80///
81/// Two surface forms are supported:
82///
83/// 1. **Trait-impl form:** `#[task] impl Task<S> for X { ... }` — user
84/// writes the trait header.
85/// 2. **Inherent-impl form:** `#[task(state = S [, key = K])] impl X { ... }` —
86/// user writes only the inherent block; the macro builds the trait header
87/// and enforces that exactly one of `run` / `run_bare` is present.
88///
89/// For compensatable (saga) tasks, use [`#[cano::saga::task]`](compensatable_task)
90/// instead.
91///
92/// # Example
93///
94/// ```ignore
95/// use cano::task;
96///
97/// #[task(state = MyState)]
98/// impl MyTask {
99/// async fn run_bare(&self) -> Result<TaskResult<MyState>, CanoError> {
100/// Ok(TaskResult::Single(MyState::Done))
101/// }
102/// }
103/// ```
104#[proc_macro_attribute]
105pub fn task(attr: TokenStream, item: TokenStream) -> TokenStream {
106 // Try the inherent-impl path when attr args are present, OR when the impl
107 // block is an inherent (non-trait) impl. Otherwise fall through to the
108 // plain async rewriter.
109 if let Ok(item_impl) = syn::parse::<syn::ItemImpl>(item.clone()) {
110 let attr2: proc_macro2::TokenStream = attr.into();
111 let has_attr = !attr2.is_empty();
112 let is_inherent = item_impl.trait_.is_none();
113 if has_attr || is_inherent {
114 return task_impl::expand(attr2, item.into())
115 .unwrap_or_else(syn::Error::into_compile_error)
116 .into();
117 }
118 }
119 async_rewrite::rewrite(item)
120}
121
122/// Apply to `impl Resource for ...` blocks (or the `Resource` trait definition itself).
123///
124/// Rewrites every `async fn` method into a method returning
125/// `Pin<Box<dyn Future<Output = ...> + Send + 'async_trait>>`. Behaviorally
126/// identical to [`task`]; the separate name makes the attribute
127/// self-documenting at impl sites.
128#[proc_macro_attribute]
129pub fn resource(_attr: TokenStream, item: TokenStream) -> TokenStream {
130 async_rewrite::rewrite(item)
131}
132
133/// Apply to the `CheckpointStore` trait definition, an
134/// `impl CheckpointStore for T` block, or — for less boilerplate — an inherent
135/// `impl T { ... }` block.
136///
137/// Two surface forms on impl blocks:
138///
139/// 1. **Trait-impl form:** `#[checkpoint_store] impl CheckpointStore for T { ... }` —
140/// user writes the trait header.
141/// 2. **Inherent-impl form:** `#[checkpoint_store] impl T { async fn append(..); async fn
142/// load_run(..); async fn clear(..); }` — the macro builds the `impl CheckpointStore for T`
143/// header and enforces that all three methods are present. (`CheckpointStore` takes no type
144/// parameters, so there are no attribute args.)
145///
146/// Either way, every `async fn` is rewritten to return
147/// `Pin<Box<dyn Future<Output = ...> + Send + 'async_trait>>`. On the trait definition the macro
148/// just performs that rewrite.
149#[proc_macro_attribute]
150pub fn checkpoint_store(_attr: TokenStream, item: TokenStream) -> TokenStream {
151 if let Ok(item_impl) = syn::parse::<syn::ItemImpl>(item.clone())
152 && item_impl.trait_.is_none()
153 {
154 return checkpoint_store_impl::expand(item.into())
155 .unwrap_or_else(syn::Error::into_compile_error)
156 .into();
157 }
158 async_rewrite::rewrite(item)
159}
160
161/// Apply to the `CompensatableTask` trait definition, an
162/// `impl CompensatableTask<S [, K]> for T` block, or — for less boilerplate — an
163/// inherent `impl T { ... }` block.
164///
165/// Use as `#[cano::saga::task]`.
166///
167/// Two surface forms on impl blocks:
168///
169/// 1. **Trait-impl form:** `#[saga::task] impl CompensatableTask<S> for T { type Output =
170/// O; async fn run(..); async fn compensate(..); }` — user writes the trait header.
171/// 2. **Inherent-impl form:** `#[saga::task(state = S [, key = K])] impl T { type Output =
172/// O; async fn run(..); async fn compensate(..); }` — the macro builds the
173/// `impl CompensatableTask<S [, K]> for T` header from the attribute args and enforces that
174/// `type Output`, `run`, and `compensate` are present (`config` / `name` may be overridden).
175///
176/// Either way, every `async fn` is rewritten to return
177/// `Pin<Box<dyn Future<Output = ...> + Send + 'async_trait>>`. On the trait definition the macro
178/// just performs that rewrite.
179#[proc_macro_attribute]
180pub fn compensatable_task(attr: TokenStream, item: TokenStream) -> TokenStream {
181 if let Ok(item_impl) = syn::parse::<syn::ItemImpl>(item.clone()) {
182 let attr2: proc_macro2::TokenStream = attr.into();
183 let is_inherent = item_impl.trait_.is_none();
184 if is_inherent || !attr2.is_empty() {
185 return compensatable_task_impl::expand(attr2, item.into())
186 .unwrap_or_else(syn::Error::into_compile_error)
187 .into();
188 }
189 }
190 async_rewrite::rewrite(item)
191}
192
193/// Apply to the `RouterTask` trait definition, an
194/// `impl RouterTask<S [, K]> for T` block, or — for less boilerplate — an
195/// inherent `impl T { ... }` block.
196///
197/// Use as `#[cano::task::router]`.
198///
199/// Two surface forms on impl blocks:
200///
201/// 1. **Trait-impl form:** `#[task::router] impl RouterTask<S> for T { async fn route(..) { ... } }` —
202/// user writes the trait header. The macro async-rewrites the `RouterTask` impl AND emits a
203/// companion `impl Task<S> for T` that delegates `Task::run` → `RouterTask::route`.
204/// 2. **Inherent-impl form:** `#[task::router(state = S [, key = K])] impl T { async fn route(..) { ... } }` —
205/// the macro builds the `impl RouterTask<S [, K]> for T` header from the attribute args, enforces
206/// that `route` is present (`config` / `name` may be overridden), and emits the same companion
207/// `impl Task<S [, K]> for T`.
208///
209/// On a trait definition (`#[task::router] pub trait RouterTask ...`) the macro just performs the
210/// async-fn-in-trait rewrite.
211///
212/// Because a blanket `impl<R: RouterTask<..>> Task<..> for R` would conflict (E0119) with the
213/// analogous blanket impls for the other specialized task traits — a type can implement more than
214/// one — the companion `Task` impl is generated per-use-site rather than as a blanket.
215#[proc_macro_attribute]
216pub fn router_task(attr: TokenStream, item: TokenStream) -> TokenStream {
217 router_task_impl::expand(attr.into(), item.into())
218 .unwrap_or_else(syn::Error::into_compile_error)
219 .into()
220}
221
222/// Apply to the `BatchTask` trait definition, an
223/// `impl BatchTask<S [, K]> for T` block, or — for less boilerplate — an
224/// inherent `impl T { ... }` block.
225///
226/// Use as `#[cano::task::batch]`.
227///
228/// Two surface forms are supported on impl blocks:
229///
230/// 1. **Trait-impl form:** `#[task::batch] impl BatchTask<S> for T { type Item = I; type ItemOutput = O; ... }` —
231/// user writes the trait header. The macro async-rewrites the `BatchTask` impl AND emits a
232/// companion `impl Task<S> for T` that delegates `Task::run` → `run_batch`.
233/// 2. **Inherent-impl form:** `#[task::batch(state = S [, key = K])] impl T { async fn load(..); async fn process_item(..); async fn finish(..); }` —
234/// the macro builds the `impl BatchTask<S [, K]> for T` header from the attribute args,
235/// infers `type Item` from the `&T` parameter of `process_item` and `type ItemOutput` from
236/// its return type, enforces that `load`, `process_item`, and `finish` are present
237/// (`concurrency`, `item_retry`, `config`, `name` may be overridden), and emits the same
238/// companion `impl Task<S [, K]> for T`.
239///
240/// On a trait definition (`#[task::batch] pub trait BatchTask ...`) the macro just performs the
241/// async-fn-in-trait rewrite.
242///
243/// Because a blanket `impl<B: BatchTask<..>> Task<..> for B` would conflict (E0119) with the
244/// analogous blanket impls for the other specialized task traits — a type can implement more than
245/// one — the companion `Task` impl is generated per-use-site rather than as a blanket.
246///
247/// `Task::run` always delegates to the `::cano::task::batch::run_batch` free function (it is
248/// never inlined into the generated code) because the fan-out loop requires `futures_util`,
249/// which is not a direct dependency of external callers.
250#[proc_macro_attribute]
251pub fn batch_task(attr: TokenStream, item: TokenStream) -> TokenStream {
252 batch_task_impl::expand(attr.into(), item.into())
253 .unwrap_or_else(syn::Error::into_compile_error)
254 .into()
255}
256
257/// Apply to the `PollTask` trait definition, an
258/// `impl PollTask<S [, K]> for T` block, or — for less boilerplate — an
259/// inherent `impl T { ... }` block.
260///
261/// Use as `#[cano::task::poll]`.
262///
263/// Two surface forms on impl blocks:
264///
265/// 1. **Trait-impl form:** `#[task::poll] impl PollTask<S> for T { async fn poll(..) { ... } }` —
266/// user writes the trait header. The macro async-rewrites the `PollTask` impl AND emits a
267/// companion `impl Task<S> for T` that delegates `Task::run` via the `run_poll_loop` helper.
268/// 2. **Inherent-impl form:** `#[task::poll(state = S [, key = K])] impl T { async fn poll(..) { ... } }` —
269/// the macro builds the `impl PollTask<S [, K]> for T` header from the attribute args, enforces
270/// that `poll` is present (`config` / `name` may be overridden), and emits the same companion
271/// `impl Task<S [, K]> for T`.
272///
273/// On a trait definition (`#[task::poll] pub trait PollTask ...`) the macro just performs the
274/// async-fn-in-trait rewrite.
275///
276/// Because a blanket `impl<P: PollTask<..>> Task<..> for P` would conflict (E0119) with the
277/// analogous blanket impls for the other specialized task traits — a type can implement more than
278/// one — the companion `Task` impl is generated per-use-site rather than as a blanket.
279///
280/// The default `config()` injected by the inherent form is [`TaskConfig::minimal()`]
281/// (no retries) — the poll loop itself is the resilience mechanism.
282#[proc_macro_attribute]
283pub fn poll_task(attr: TokenStream, item: TokenStream) -> TokenStream {
284 poll_task_impl::expand(attr.into(), item.into())
285 .unwrap_or_else(syn::Error::into_compile_error)
286 .into()
287}
288
289/// Apply to the `TimerTask` trait definition, an
290/// `impl TimerTask<S [, K]> for T` block, or — for less boilerplate — an
291/// inherent `impl T { ... }` block.
292///
293/// Use as `#[cano::task::timer]`.
294///
295/// Two surface forms on impl blocks:
296///
297/// 1. **Trait-impl form:** `#[task::timer] impl TimerTask<S> for T { async fn wait(..) { ... } async fn after_wait(..) { ... } }` —
298/// user writes the trait header. The macro async-rewrites the `TimerTask` impl AND emits a
299/// companion `impl Task<S> for T` whose `Task::run` sleeps for the returned `TimerOutcome`
300/// (via the `run_timer` helper) then calls `after_wait`.
301/// 2. **Inherent-impl form:** `#[task::timer(state = S [, key = K])] impl T { async fn wait(..) { ... } async fn after_wait(..) { ... } }` —
302/// the macro builds the `impl TimerTask<S [, K]> for T` header from the attribute args, enforces
303/// that both `wait` and `after_wait` are present (`config` / `name` may be overridden), and emits
304/// the same companion `impl Task<S [, K]> for T`.
305///
306/// On a trait definition (`#[task::timer] pub trait TimerTask ...`) the macro just performs the
307/// async-fn-in-trait rewrite.
308///
309/// Because a blanket `impl<T: TimerTask<..>> Task<..> for T` would conflict (E0119) with the
310/// analogous blanket impls for the other specialized task traits — a type can implement more than
311/// one — the companion `Task` impl is generated per-use-site rather than as a blanket.
312///
313/// The default `config()` injected by the inherent form is [`TaskConfig::minimal()`]
314/// (no retries) — a single scheduled sleep needs no outer retry wrapping.
315///
316/// The **inherent form** routes `Task::run` through `::cano::task::timer::run_timer`, so the user
317/// crate needs no direct `tokio` dependency. The **trait-impl form** inlines the sleep, so a crate
318/// using `#[task::timer] impl TimerTask<S> for T` must depend on `tokio` (feature `time`),
319/// reachable as `::tokio` — the same requirement the sibling `#[task::poll]` macro has.
320#[proc_macro_attribute]
321pub fn timer_task(attr: TokenStream, item: TokenStream) -> TokenStream {
322 timer_task_impl::expand(attr.into(), item.into())
323 .unwrap_or_else(syn::Error::into_compile_error)
324 .into()
325}
326
327/// Apply to the `SteppedTask` trait definition, an
328/// `impl SteppedTask<S [, K]> for T` block, or — for less boilerplate — an
329/// inherent `impl T { ... }` block.
330///
331/// Use as `#[cano::task::stepped]`.
332///
333/// Two surface forms on impl blocks:
334///
335/// 1. **Trait-impl form:** `#[task::stepped] impl SteppedTask<S> for T { type Cursor = C; async fn step(..) { ... } }` —
336/// user writes the trait header. The macro async-rewrites the `SteppedTask` impl AND emits a
337/// companion `impl Task<S> for T` that delegates `Task::run` via the `run_stepped` helper.
338/// 2. **Inherent-impl form:** `#[task::stepped(state = S [, key = K])] impl T { async fn step(..) { ... } }` —
339/// the macro builds the `impl SteppedTask<S [, K]> for T` header from the attribute args, infers
340/// `type Cursor` from the `Option<C>` third parameter of `step`, enforces that `step` is present
341/// (`config` / `name` may be overridden), and emits the same companion `impl Task<S [, K]> for T`.
342///
343/// On a trait definition (`#[task::stepped] pub trait SteppedTask ...`) the macro just performs the
344/// async-fn-in-trait rewrite.
345///
346/// Because a blanket `impl<S: SteppedTask<..>> Task<..> for S` would conflict (E0119) with the
347/// analogous blanket impls for the other specialized task traits — a type can implement more than
348/// one — the companion `Task` impl is generated per-use-site rather than as a blanket.
349///
350/// The default `config()` injected by the inherent form is [`TaskConfig::default()`]
351/// (exponential backoff with 3 retries).
352#[proc_macro_attribute]
353pub fn stepped_task(attr: TokenStream, item: TokenStream) -> TokenStream {
354 stepped_task_impl::expand(attr.into(), item.into())
355 .unwrap_or_else(syn::Error::into_compile_error)
356 .into()
357}
358
359/// Apply to the `StreamTask` trait definition, an `impl StreamTask<S [, K]> for T`
360/// block, or an inherent `impl T { ... }` block.
361///
362/// Use as `#[cano::task::stream]`. `StreamTask` is a genuine stream-processing model:
363/// consume an `impl Stream` continuously, flush per [`StreamWindow`] window, run until
364/// the workflow's `CancellationToken` fires, and persist a resumable cursor (via
365/// [`Workflow::register_stream`]). Per-item errors are governed by [`StreamErrorPolicy`].
366///
367/// Two surface forms on impl blocks:
368///
369/// 1. **Trait-impl form:** `#[task::stream] impl StreamTask<S> for T { type Item = ..; .. }`.
370/// 2. **Inherent-impl form:** `#[task::stream(state = S [, key = K])] impl T { async fn open(..) .. }` —
371/// the macro infers `type Item` from `process_item`'s owned `item` parameter and
372/// `type Output` / `type Cursor` from the `Ok` 2-tuple of `process_item`'s return,
373/// requires `open` / `process_item` / `flush_window` / `on_close`, and emits a
374/// companion `impl Task<S [, K]> for T` whose `run` forwards to `StreamTask::run_in_memory`.
375///
376/// On a trait definition the macro just performs the async-fn-in-trait rewrite.
377///
378/// The default `config()` injected by the inherent form is [`TaskConfig::minimal()`]
379/// (no outer retry — like `PollTask`; an outer retry would re-invoke `open()`).
380#[proc_macro_attribute]
381pub fn stream_task(attr: TokenStream, item: TokenStream) -> TokenStream {
382 stream_task_impl::expand(attr.into(), item.into())
383 .unwrap_or_else(syn::Error::into_compile_error)
384 .into()
385}
386
387/// Derive an empty `cano::Resource` impl (uses the trait's default no-op
388/// `setup` / `teardown`).
389///
390/// Apply this derive to any struct that needs to implement `Resource` but has no
391/// custom lifecycle logic. The trait's `setup` and `teardown` defaults (which
392/// return `Ok(())`) take effect automatically.
393///
394/// # Example
395///
396/// ```ignore
397/// use cano::prelude::*;
398///
399/// #[derive(Resource)]
400/// struct MyConfig {
401/// timeout_ms: u64,
402/// }
403/// ```
404#[proc_macro_derive(Resource)]
405pub fn derive_resource(input: TokenStream) -> TokenStream {
406 resource_derive::expand(input.into())
407 .unwrap_or_else(syn::Error::into_compile_error)
408 .into()
409}