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//! Available macros:
13//!
14//! - [`task`] — for `impl Task` (and the `Task` trait definition itself)
15//! - [`node`] — for `impl Node` and the `Node` trait
16//! - [`resource`] — for `impl Resource` and the `Resource` trait
17//!
18//! All three are functionally identical; they differ only in name. New traits
19//! that need async-fn-in-dyn rewriting can ship their own `cano-macros`
20//! attribute alongside.
21
22use proc_macro::TokenStream;
23
24mod async_rewrite;
25mod attr_args;
26mod from_resources;
27mod node_impl;
28mod resource_derive;
29mod task_impl;
30
31/// Derive a `from_resources(&Resources<_>) -> CanoResult<Self>` constructor that
32/// pulls each field out of a `cano::Resources` map.
33///
34/// Each field must be `Arc<T>`. Use `#[res("key")]` for string-literal lookups
35/// or `#[res(EnumType::Variant)]` for enum-path lookups. Use
36/// `#[from_resources(key = MyType)]` on the struct to override the inferred key type.
37///
38/// # Example
39///
40/// ```ignore
41/// use cano::prelude::*;
42/// use std::sync::Arc;
43///
44/// #[derive(FromResources)]
45/// struct Deps {
46/// #[res("store")]
47/// store: Arc<MemoryStore>,
48/// }
49/// ```
50#[proc_macro_derive(FromResources, attributes(res, from_resources))]
51pub fn derive_from_resources(input: TokenStream) -> TokenStream {
52 from_resources::expand(input.into())
53 .unwrap_or_else(syn::Error::into_compile_error)
54 .into()
55}
56
57/// Apply to `impl Task for ...` blocks (or the `Task` trait definition itself).
58///
59/// Rewrites every `async fn` method into a method returning
60/// `Pin<Box<dyn Future<Output = ...> + Send + 'async_trait>>`, the same shape
61/// `async-trait` produces. This makes the methods callable through
62/// `dyn Task<...>`.
63///
64/// Two surface forms are supported:
65///
66/// 1. **Trait-impl form (legacy):** `#[task] impl Task<S> for X { ... }` — user
67/// writes the trait header.
68/// 2. **Inherent-impl form:** `#[task(state = S [, key = K])] impl X { ... }` —
69/// user writes only the inherent block; the macro builds the trait header
70/// and enforces that exactly one of `run` / `run_bare` is present.
71///
72/// # Example
73///
74/// ```ignore
75/// use cano::task;
76///
77/// #[task(state = MyState)]
78/// impl MyTask {
79/// async fn run_bare(&self) -> Result<TaskResult<MyState>, CanoError> {
80/// Ok(TaskResult::Single(MyState::Done))
81/// }
82/// }
83/// ```
84#[proc_macro_attribute]
85pub fn task(attr: TokenStream, item: TokenStream) -> TokenStream {
86 // Try the inherent-impl path when attr args are present, OR when the impl
87 // block is an inherent (non-trait) impl. Otherwise fall through to the
88 // plain async rewriter.
89 if let Ok(item_impl) = syn::parse::<syn::ItemImpl>(item.clone()) {
90 let attr2: proc_macro2::TokenStream = attr.into();
91 let has_attr = !attr2.is_empty();
92 let is_inherent = item_impl.trait_.is_none();
93 if has_attr || is_inherent {
94 return task_impl::expand(attr2, item.into())
95 .unwrap_or_else(syn::Error::into_compile_error)
96 .into();
97 }
98 }
99 async_rewrite::rewrite(item)
100}
101
102/// Apply to `impl Node for ...` blocks, inherent `impl X { ... }` blocks, or
103/// the `Node` trait definition itself.
104///
105/// Two surface forms are supported on impl blocks:
106///
107/// 1. **Trait-impl form (legacy):** `#[node] impl Node<S> for X { ... }`. The
108/// macro infers `type PrepResult` / `type ExecResult` from the return types
109/// of `prep` and `exec`, and supplies a default `fn config(&self) -> TaskConfig`
110/// when missing.
111/// 2. **Inherent-impl form:** `#[node(state = S [, key = K])] impl X { ... }`.
112/// The macro builds the `impl Node<S [, K]> for X` header from the attribute
113/// args, enforces that `prep` / `exec` / `post` are present, and injects
114/// the same boilerplate as form 1.
115///
116/// On a trait definition (`#[node] pub trait Node ...`) the macro just performs
117/// the async-fn-in-trait rewrite.
118#[proc_macro_attribute]
119pub fn node(attr: TokenStream, item: TokenStream) -> TokenStream {
120 if let Ok(item_impl) = syn::parse::<syn::ItemImpl>(item.clone()) {
121 let attr2: proc_macro2::TokenStream = attr.into();
122 let has_attr = !attr2.is_empty();
123 let is_inherent = item_impl.trait_.is_none();
124
125 // Inherent form: always go through the boilerplate filler (it builds
126 // the trait header from attr args and enforces mandatory methods).
127 if is_inherent || has_attr {
128 return node_impl::expand(attr2, item.into())
129 .unwrap_or_else(syn::Error::into_compile_error)
130 .into();
131 }
132
133 // Trait-impl form: only run the boilerplate filler when inference is
134 // actually needed (at least one of PrepResult/ExecResult is absent).
135 let has_prep = item_impl
136 .items
137 .iter()
138 .any(|it| matches!(it, syn::ImplItem::Type(t) if t.ident == "PrepResult"));
139 let has_exec = item_impl
140 .items
141 .iter()
142 .any(|it| matches!(it, syn::ImplItem::Type(t) if t.ident == "ExecResult"));
143 if !has_prep || !has_exec {
144 return node_impl::expand(proc_macro2::TokenStream::new(), item.into())
145 .unwrap_or_else(syn::Error::into_compile_error)
146 .into();
147 }
148 }
149 async_rewrite::rewrite(item)
150}
151
152/// Apply to `impl Resource for ...` blocks (or the `Resource` trait definition itself).
153///
154/// Rewrites every `async fn` method into a method returning
155/// `Pin<Box<dyn Future<Output = ...> + Send + 'async_trait>>`. Behaviorally
156/// identical to [`task`] and [`node`]; the separate name makes the attribute
157/// self-documenting at impl sites.
158#[proc_macro_attribute]
159pub fn resource(_attr: TokenStream, item: TokenStream) -> TokenStream {
160 async_rewrite::rewrite(item)
161}
162
163/// Derive an empty `cano::Resource` impl (uses the trait's default no-op
164/// `setup` / `teardown`).
165///
166/// Apply this derive to any struct that needs to implement `Resource` but has no
167/// custom lifecycle logic. The trait's `setup` and `teardown` defaults (which
168/// return `Ok(())`) take effect automatically.
169///
170/// # Example
171///
172/// ```ignore
173/// use cano::prelude::*;
174///
175/// #[derive(Resource)]
176/// struct MyConfig {
177/// timeout_ms: u64,
178/// }
179/// ```
180#[proc_macro_derive(Resource)]
181pub fn derive_resource(input: TokenStream) -> TokenStream {
182 resource_derive::expand(input.into())
183 .unwrap_or_else(syn::Error::into_compile_error)
184 .into()
185}