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::batch]` — for `impl BatchTask` and the `BatchTask` trait
18//! - `#[cano::task::stepped]` — for `impl SteppedTask` and the `SteppedTask` trait
19//! - `#[cano::saga::task]` — for `impl CompensatableTask`
20//! - `#[cano::resource]` — for `impl Resource` and the `Resource` trait
21//! - `#[cano::checkpoint_store]` — for `impl CheckpointStore` and the `CheckpointStore` trait
22//!
23//! All are functionally identical in the async-rewrite they perform; they differ
24//! only in name. New traits that need async-fn-in-dyn rewriting can ship their
25//! own `cano-macros` attribute alongside.
26
27use proc_macro::TokenStream;
28
29mod async_rewrite;
30mod attr_args;
31mod batch_task_impl;
32mod checkpoint_store_impl;
33mod compensatable_task_impl;
34mod from_resources;
35mod path_prefix;
36mod poll_task_impl;
37mod resource_derive;
38mod router_task_impl;
39mod stepped_task_impl;
40mod task_impl;
41
42/// Derive a `from_resources(&Resources<_>) -> CanoResult<Self>` constructor that
43/// pulls each field out of a `cano::Resources` map.
44///
45/// Each field must be `Arc<T>`. Use `#[res("key")]` for string-literal lookups
46/// or `#[res(EnumType::Variant)]` for enum-path lookups. Use
47/// `#[from_resources(key = MyType)]` on the struct to override the inferred key type.
48///
49/// # Example
50///
51/// ```ignore
52/// use cano::prelude::*;
53/// use std::sync::Arc;
54///
55/// #[derive(FromResources)]
56/// struct Deps {
57/// #[res("store")]
58/// store: Arc<MemoryStore>,
59/// }
60/// ```
61#[proc_macro_derive(FromResources, attributes(res, from_resources))]
62pub fn derive_from_resources(input: TokenStream) -> TokenStream {
63 from_resources::expand(input.into())
64 .unwrap_or_else(syn::Error::into_compile_error)
65 .into()
66}
67
68/// Apply to `impl Task for ...` blocks (or the `Task` trait definition itself).
69///
70/// Rewrites every `async fn` method into a method returning
71/// `Pin<Box<dyn Future<Output = ...> + Send + 'async_trait>>`, the same shape
72/// `async-trait` produces. This makes the methods callable through
73/// `dyn Task<...>`.
74///
75/// Use as `#[cano::task]`.
76///
77/// Two surface forms are supported:
78///
79/// 1. **Trait-impl form:** `#[task] impl Task<S> for X { ... }` — user
80/// writes the trait header.
81/// 2. **Inherent-impl form:** `#[task(state = S [, key = K])] impl X { ... }` —
82/// user writes only the inherent block; the macro builds the trait header
83/// and enforces that exactly one of `run` / `run_bare` is present.
84///
85/// For compensatable (saga) tasks, use [`#[cano::saga::task]`](compensatable_task)
86/// instead.
87///
88/// # Example
89///
90/// ```ignore
91/// use cano::task;
92///
93/// #[task(state = MyState)]
94/// impl MyTask {
95/// async fn run_bare(&self) -> Result<TaskResult<MyState>, CanoError> {
96/// Ok(TaskResult::Single(MyState::Done))
97/// }
98/// }
99/// ```
100#[proc_macro_attribute]
101pub fn task(attr: TokenStream, item: TokenStream) -> TokenStream {
102 // Try the inherent-impl path when attr args are present, OR when the impl
103 // block is an inherent (non-trait) impl. Otherwise fall through to the
104 // plain async rewriter.
105 if let Ok(item_impl) = syn::parse::<syn::ItemImpl>(item.clone()) {
106 let attr2: proc_macro2::TokenStream = attr.into();
107 let has_attr = !attr2.is_empty();
108 let is_inherent = item_impl.trait_.is_none();
109 if has_attr || is_inherent {
110 return task_impl::expand(attr2, item.into())
111 .unwrap_or_else(syn::Error::into_compile_error)
112 .into();
113 }
114 }
115 async_rewrite::rewrite(item)
116}
117
118/// Apply to `impl Resource for ...` blocks (or the `Resource` trait definition itself).
119///
120/// Rewrites every `async fn` method into a method returning
121/// `Pin<Box<dyn Future<Output = ...> + Send + 'async_trait>>`. Behaviorally
122/// identical to [`task`]; the separate name makes the attribute
123/// self-documenting at impl sites.
124#[proc_macro_attribute]
125pub fn resource(_attr: TokenStream, item: TokenStream) -> TokenStream {
126 async_rewrite::rewrite(item)
127}
128
129/// Apply to the `CheckpointStore` trait definition, an
130/// `impl CheckpointStore for T` block, or — for less boilerplate — an inherent
131/// `impl T { ... }` block.
132///
133/// Two surface forms on impl blocks:
134///
135/// 1. **Trait-impl form:** `#[checkpoint_store] impl CheckpointStore for T { ... }` —
136/// user writes the trait header.
137/// 2. **Inherent-impl form:** `#[checkpoint_store] impl T { async fn append(..); async fn
138/// load_run(..); async fn clear(..); }` — the macro builds the `impl CheckpointStore for T`
139/// header and enforces that all three methods are present. (`CheckpointStore` takes no type
140/// parameters, so there are no attribute args.)
141///
142/// Either way, every `async fn` is rewritten to return
143/// `Pin<Box<dyn Future<Output = ...> + Send + 'async_trait>>`. On the trait definition the macro
144/// just performs that rewrite.
145#[proc_macro_attribute]
146pub fn checkpoint_store(_attr: TokenStream, item: TokenStream) -> TokenStream {
147 if let Ok(item_impl) = syn::parse::<syn::ItemImpl>(item.clone())
148 && item_impl.trait_.is_none()
149 {
150 return checkpoint_store_impl::expand(item.into())
151 .unwrap_or_else(syn::Error::into_compile_error)
152 .into();
153 }
154 async_rewrite::rewrite(item)
155}
156
157/// Apply to the `CompensatableTask` trait definition, an
158/// `impl CompensatableTask<S [, K]> for T` block, or — for less boilerplate — an
159/// inherent `impl T { ... }` block.
160///
161/// Use as `#[cano::saga::task]`.
162///
163/// Two surface forms on impl blocks:
164///
165/// 1. **Trait-impl form:** `#[saga::task] impl CompensatableTask<S> for T { type Output =
166/// O; async fn run(..); async fn compensate(..); }` — user writes the trait header.
167/// 2. **Inherent-impl form:** `#[saga::task(state = S [, key = K])] impl T { type Output =
168/// O; async fn run(..); async fn compensate(..); }` — the macro builds the
169/// `impl CompensatableTask<S [, K]> for T` header from the attribute args and enforces that
170/// `type Output`, `run`, and `compensate` are present (`config` / `name` may be overridden).
171///
172/// Either way, every `async fn` is rewritten to return
173/// `Pin<Box<dyn Future<Output = ...> + Send + 'async_trait>>`. On the trait definition the macro
174/// just performs that rewrite.
175#[proc_macro_attribute]
176pub fn compensatable_task(attr: TokenStream, item: TokenStream) -> TokenStream {
177 if let Ok(item_impl) = syn::parse::<syn::ItemImpl>(item.clone()) {
178 let attr2: proc_macro2::TokenStream = attr.into();
179 let is_inherent = item_impl.trait_.is_none();
180 if is_inherent || !attr2.is_empty() {
181 return compensatable_task_impl::expand(attr2, item.into())
182 .unwrap_or_else(syn::Error::into_compile_error)
183 .into();
184 }
185 }
186 async_rewrite::rewrite(item)
187}
188
189/// Apply to the `RouterTask` trait definition, an
190/// `impl RouterTask<S [, K]> for T` block, or — for less boilerplate — an
191/// inherent `impl T { ... }` block.
192///
193/// Use as `#[cano::task::router]`.
194///
195/// Two surface forms on impl blocks:
196///
197/// 1. **Trait-impl form:** `#[task::router] impl RouterTask<S> for T { async fn route(..) { ... } }` —
198/// user writes the trait header. The macro async-rewrites the `RouterTask` impl AND emits a
199/// companion `impl Task<S> for T` that delegates `Task::run` → `RouterTask::route`.
200/// 2. **Inherent-impl form:** `#[task::router(state = S [, key = K])] impl T { async fn route(..) { ... } }` —
201/// the macro builds the `impl RouterTask<S [, K]> for T` header from the attribute args, enforces
202/// that `route` is present (`config` / `name` may be overridden), and emits the same companion
203/// `impl Task<S [, K]> for T`.
204///
205/// On a trait definition (`#[task::router] pub trait RouterTask ...`) the macro just performs the
206/// async-fn-in-trait rewrite.
207///
208/// Because a blanket `impl<R: RouterTask<..>> Task<..> for R` would conflict (E0119) with the
209/// analogous blanket impls for the other specialized task traits — a type can implement more than
210/// one — the companion `Task` impl is generated per-use-site rather than as a blanket.
211#[proc_macro_attribute]
212pub fn router_task(attr: TokenStream, item: TokenStream) -> TokenStream {
213 router_task_impl::expand(attr.into(), item.into())
214 .unwrap_or_else(syn::Error::into_compile_error)
215 .into()
216}
217
218/// Apply to the `BatchTask` trait definition, an
219/// `impl BatchTask<S [, K]> for T` block, or — for less boilerplate — an
220/// inherent `impl T { ... }` block.
221///
222/// Use as `#[cano::task::batch]`.
223///
224/// Two surface forms are supported on impl blocks:
225///
226/// 1. **Trait-impl form:** `#[task::batch] impl BatchTask<S> for T { type Item = I; type ItemOutput = O; ... }` —
227/// user writes the trait header. The macro async-rewrites the `BatchTask` impl AND emits a
228/// companion `impl Task<S> for T` that delegates `Task::run` → `run_batch`.
229/// 2. **Inherent-impl form:** `#[task::batch(state = S [, key = K])] impl T { async fn load(..); async fn process_item(..); async fn finish(..); }` —
230/// the macro builds the `impl BatchTask<S [, K]> for T` header from the attribute args,
231/// infers `type Item` from the `&T` parameter of `process_item` and `type ItemOutput` from
232/// its return type, enforces that `load`, `process_item`, and `finish` are present
233/// (`concurrency`, `item_retry`, `config`, `name` may be overridden), and emits the same
234/// companion `impl Task<S [, K]> for T`.
235///
236/// On a trait definition (`#[task::batch] pub trait BatchTask ...`) the macro just performs the
237/// async-fn-in-trait rewrite.
238///
239/// Because a blanket `impl<B: BatchTask<..>> Task<..> for B` would conflict (E0119) with the
240/// analogous blanket impls for the other specialized task traits — a type can implement more than
241/// one — the companion `Task` impl is generated per-use-site rather than as a blanket.
242///
243/// `Task::run` always delegates to the `::cano::task::batch::run_batch` free function (it is
244/// never inlined into the generated code) because the fan-out loop requires `futures_util`,
245/// which is not a direct dependency of external callers.
246#[proc_macro_attribute]
247pub fn batch_task(attr: TokenStream, item: TokenStream) -> TokenStream {
248 batch_task_impl::expand(attr.into(), item.into())
249 .unwrap_or_else(syn::Error::into_compile_error)
250 .into()
251}
252
253/// Apply to the `PollTask` trait definition, an
254/// `impl PollTask<S [, K]> for T` block, or — for less boilerplate — an
255/// inherent `impl T { ... }` block.
256///
257/// Use as `#[cano::task::poll]`.
258///
259/// Two surface forms on impl blocks:
260///
261/// 1. **Trait-impl form:** `#[task::poll] impl PollTask<S> for T { async fn poll(..) { ... } }` —
262/// user writes the trait header. The macro async-rewrites the `PollTask` impl AND emits a
263/// companion `impl Task<S> for T` that delegates `Task::run` via the `run_poll_loop` helper.
264/// 2. **Inherent-impl form:** `#[task::poll(state = S [, key = K])] impl T { async fn poll(..) { ... } }` —
265/// the macro builds the `impl PollTask<S [, K]> for T` header from the attribute args, enforces
266/// that `poll` is present (`config` / `name` may be overridden), and emits the same companion
267/// `impl Task<S [, K]> for T`.
268///
269/// On a trait definition (`#[task::poll] pub trait PollTask ...`) the macro just performs the
270/// async-fn-in-trait rewrite.
271///
272/// Because a blanket `impl<P: PollTask<..>> Task<..> for P` would conflict (E0119) with the
273/// analogous blanket impls for the other specialized task traits — a type can implement more than
274/// one — the companion `Task` impl is generated per-use-site rather than as a blanket.
275///
276/// The default `config()` injected by the inherent form is [`TaskConfig::minimal()`]
277/// (no retries) — the poll loop itself is the resilience mechanism.
278#[proc_macro_attribute]
279pub fn poll_task(attr: TokenStream, item: TokenStream) -> TokenStream {
280 poll_task_impl::expand(attr.into(), item.into())
281 .unwrap_or_else(syn::Error::into_compile_error)
282 .into()
283}
284
285/// Apply to the `SteppedTask` trait definition, an
286/// `impl SteppedTask<S [, K]> for T` block, or — for less boilerplate — an
287/// inherent `impl T { ... }` block.
288///
289/// Use as `#[cano::task::stepped]`.
290///
291/// Two surface forms on impl blocks:
292///
293/// 1. **Trait-impl form:** `#[task::stepped] impl SteppedTask<S> for T { type Cursor = C; async fn step(..) { ... } }` —
294/// user writes the trait header. The macro async-rewrites the `SteppedTask` impl AND emits a
295/// companion `impl Task<S> for T` that delegates `Task::run` via the `run_stepped` helper.
296/// 2. **Inherent-impl form:** `#[task::stepped(state = S [, key = K])] impl T { async fn step(..) { ... } }` —
297/// the macro builds the `impl SteppedTask<S [, K]> for T` header from the attribute args, infers
298/// `type Cursor` from the `Option<C>` third parameter of `step`, enforces that `step` is present
299/// (`config` / `name` may be overridden), and emits the same companion `impl Task<S [, K]> for T`.
300///
301/// On a trait definition (`#[task::stepped] pub trait SteppedTask ...`) the macro just performs the
302/// async-fn-in-trait rewrite.
303///
304/// Because a blanket `impl<S: SteppedTask<..>> Task<..> for S` would conflict (E0119) with the
305/// analogous blanket impls for the other specialized task traits — a type can implement more than
306/// one — the companion `Task` impl is generated per-use-site rather than as a blanket.
307///
308/// The default `config()` injected by the inherent form is [`TaskConfig::default()`]
309/// (exponential backoff with 3 retries).
310#[proc_macro_attribute]
311pub fn stepped_task(attr: TokenStream, item: TokenStream) -> TokenStream {
312 stepped_task_impl::expand(attr.into(), item.into())
313 .unwrap_or_else(syn::Error::into_compile_error)
314 .into()
315}
316
317/// Derive an empty `cano::Resource` impl (uses the trait's default no-op
318/// `setup` / `teardown`).
319///
320/// Apply this derive to any struct that needs to implement `Resource` but has no
321/// custom lifecycle logic. The trait's `setup` and `teardown` defaults (which
322/// return `Ok(())`) take effect automatically.
323///
324/// # Example
325///
326/// ```ignore
327/// use cano::prelude::*;
328///
329/// #[derive(Resource)]
330/// struct MyConfig {
331/// timeout_ms: u64,
332/// }
333/// ```
334#[proc_macro_derive(Resource)]
335pub fn derive_resource(input: TokenStream) -> TokenStream {
336 resource_derive::expand(input.into())
337 .unwrap_or_else(syn::Error::into_compile_error)
338 .into()
339}