feat: dagrunner, initial version
This commit is contained in:
122 files changed
+23101
No files matched your search
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "dagx-macros"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.78.0"
|
||||
authors = ["Stephen Waits <steve@waits.net>"]
|
||||
license = "MIT"
|
||||
description = "Procedural macros for dagx"
|
||||
repository = "https://github.com/swaits/dagx"
|
||||
homepage = "https://github.com/swaits/dagx"
|
||||
documentation = "https://docs.rs/dagx-macros"
|
||||
readme = "README.md"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
syn = { version = "2", features = ["full", "extra-traits"] }
|
||||
quote = "1"
|
||||
proc-macro2 = "1"
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Stephen Waits <steve@waits.net>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,15 @@
|
||||
# dagx-macros
|
||||
|
||||
Procedural macros for [dagx](https://crates.io/crates/dagx).
|
||||
|
||||
This crate provides the `#[task]` attribute macro. You typically don't need to depend on this directly—it's included automatically when you use `dagx`.
|
||||
|
||||
## Usage
|
||||
|
||||
See the [dagx documentation](https://docs.rs/dagx) for usage examples.
|
||||
|
||||
The `#[task]` macro automatically implements the `Task` trait by deriving input and output types from your `run()` method signature.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -0,0 +1,406 @@
|
||||
//! Procedural macros for dagx
|
||||
//!
|
||||
//! This crate provides the `#[task]` attribute macro that automatically implements
|
||||
//! the `Task` trait by deriving Input and Output types from the `run()` method signature.
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{parse_macro_input, FnArg, ImplItem, ItemImpl, Pat, PatType, ReturnType, Type};
|
||||
|
||||
/// Attribute macro to automatically implement the `Task` trait.
|
||||
///
|
||||
/// Apply this to an `impl` block containing a `run()` method (sync or async). The macro:
|
||||
/// - Derives `Input` and `Output` types from the `run()` signature
|
||||
/// - Automatically implements the `Task` trait
|
||||
/// - Supports both sync and async run methods
|
||||
/// - Supports stateless (no self) and stateful (&self, &mut self) tasks
|
||||
/// - Handles various input patterns (no inputs, single input, multiple inputs)
|
||||
///
|
||||
/// # Task Patterns
|
||||
///
|
||||
/// The `#[task]` macro supports three patterns based on state requirements:
|
||||
///
|
||||
/// ## 1. Stateless Tasks (No State)
|
||||
///
|
||||
/// Unit structs for pure computations. Use **no `self` parameter**:
|
||||
///
|
||||
/// ```ignore
|
||||
/// use dagx::{task, Task};
|
||||
///
|
||||
/// struct Add;
|
||||
///
|
||||
/// #[task]
|
||||
/// impl Add {
|
||||
/// async fn run(a: &i32, b: &i32) -> i32 {
|
||||
/// a + b // Pure function, no state
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ## 2. Read-Only State Tasks
|
||||
///
|
||||
/// Tasks that read configuration or constant data. Use **`&self`**:
|
||||
///
|
||||
/// ```ignore
|
||||
/// use dagx::{task, Task};
|
||||
///
|
||||
/// struct Multiplier {
|
||||
/// factor: i32,
|
||||
/// }
|
||||
///
|
||||
/// #[task]
|
||||
/// impl Multiplier {
|
||||
/// async fn run(&self, input: &i32) -> i32 {
|
||||
/// input * self.factor // Read-only access
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ## 3. Mutable State Tasks
|
||||
///
|
||||
/// Tasks that accumulate or modify state. Use **`&mut self`**:
|
||||
///
|
||||
/// ```ignore
|
||||
/// use dagx::{task, Task};
|
||||
///
|
||||
/// struct Counter {
|
||||
/// count: i32,
|
||||
/// }
|
||||
///
|
||||
/// #[task]
|
||||
/// impl Counter {
|
||||
/// async fn run(&mut self, increment: &i32) -> i32 {
|
||||
/// self.count += increment; // Modifies state
|
||||
/// self.count
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Input Patterns
|
||||
///
|
||||
/// ## No Inputs (Source Tasks)
|
||||
///
|
||||
/// ```ignore
|
||||
/// use dagx::{task, Task};
|
||||
///
|
||||
/// struct LoadData {
|
||||
/// value: i32,
|
||||
/// }
|
||||
///
|
||||
/// #[task]
|
||||
/// impl LoadData {
|
||||
/// async fn run(&mut self) -> i32 {
|
||||
/// self.value
|
||||
/// }
|
||||
/// }
|
||||
/// ```ignore
|
||||
///
|
||||
/// ## Single Input
|
||||
///
|
||||
/// ```ignore
|
||||
/// use dagx::{task, Task};
|
||||
///
|
||||
/// struct Double;
|
||||
///
|
||||
/// #[task]
|
||||
/// impl Double {
|
||||
/// async fn run(&mut self, input: &i32) -> i32 {
|
||||
/// input * 2
|
||||
/// }
|
||||
/// }
|
||||
/// ```ignore
|
||||
///
|
||||
/// ## Multiple Inputs (up to 8)
|
||||
///
|
||||
/// ```ignore
|
||||
/// use dagx::{task, Task};
|
||||
///
|
||||
/// struct Combine;
|
||||
///
|
||||
/// #[task]
|
||||
/// impl Combine {
|
||||
/// async fn run(&mut self, a: &i32, b: &String, c: &bool) -> String {
|
||||
/// format!("{}: {} ({})", b, a, c)
|
||||
/// }
|
||||
/// }
|
||||
/// ```ignore
|
||||
///
|
||||
/// # Requirements
|
||||
///
|
||||
/// - The impl block must contain exactly one `async fn run()` method
|
||||
/// - The `run()` method can be stateless (no self parameter) or stateful (`&mut self`)
|
||||
/// - All input parameters must be references (e.g., `&i32`, not `i32`)
|
||||
/// - The macro requires `Task` to be in scope: `use dagx::Task;`
|
||||
/// - For stateless tasks, the struct must implement `Default` (e.g., unit structs)
|
||||
///
|
||||
/// # Generated Code
|
||||
///
|
||||
/// The macro transforms your implementation into a full `Task` trait implementation.
|
||||
///
|
||||
/// For stateless tasks (no self parameter):
|
||||
///
|
||||
/// ```ignore
|
||||
/// // Your code:
|
||||
/// #[task]
|
||||
/// impl Add {
|
||||
/// async fn run(a: &i32, b: &i32) -> i32 {
|
||||
/// a + b
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // Generated:
|
||||
/// impl Task for Add {
|
||||
/// type Input = (i32, i32);
|
||||
/// type Output = i32;
|
||||
///
|
||||
/// async fn run(&mut self, input: Self::Input) -> Self::Output {
|
||||
/// let (a, b) = input;
|
||||
/// Self::run_impl(&a, &b).await
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// impl Add {
|
||||
/// #[inline]
|
||||
/// async fn run_impl(a: &i32, b: &i32) -> i32 {
|
||||
/// a + b
|
||||
/// }
|
||||
/// }
|
||||
/// ```ignore
|
||||
///
|
||||
/// For stateful tasks (with &mut self):
|
||||
///
|
||||
/// ```ignore
|
||||
/// // Your code:
|
||||
/// #[task]
|
||||
/// impl Counter {
|
||||
/// async fn run(&mut self, inc: &i32) -> i32 {
|
||||
/// self.count += inc;
|
||||
/// self.count
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // Generated:
|
||||
/// impl Task for Counter {
|
||||
/// type Input = i32;
|
||||
/// type Output = i32;
|
||||
///
|
||||
/// async fn run(&mut self, input: Self::Input) -> Self::Output {
|
||||
/// let inc = input;
|
||||
/// self.run_impl(&inc).await
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// impl Counter {
|
||||
/// async fn run_impl(&mut self, inc: &i32) -> i32 {
|
||||
/// self.count += inc;
|
||||
/// self.count
|
||||
/// }
|
||||
/// }
|
||||
/// ```ignore
|
||||
#[proc_macro_attribute]
|
||||
pub fn task(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let impl_block = parse_macro_input!(item as ItemImpl);
|
||||
|
||||
// Extract the struct name
|
||||
let struct_name = &impl_block.self_ty;
|
||||
|
||||
// Find the run() method
|
||||
let run_method = match impl_block.items.iter().find_map(|item| {
|
||||
if let ImplItem::Fn(method) = item {
|
||||
if method.sig.ident == "run" {
|
||||
return Some(method);
|
||||
}
|
||||
}
|
||||
None
|
||||
}) {
|
||||
Some(method) => method,
|
||||
None => {
|
||||
return syn::Error::new_spanned(
|
||||
&impl_block,
|
||||
"impl block must contain a run() method\n\n\
|
||||
Expected signature: fn run(&mut self, ...) -> OutputType\n\
|
||||
or: async fn run(&mut self, ...) -> OutputType\n\
|
||||
The #[task] macro requires a run() method to implement the Task trait.",
|
||||
)
|
||||
.to_compile_error()
|
||||
.into();
|
||||
}
|
||||
};
|
||||
|
||||
// Check if the method is async or sync
|
||||
let is_async = run_method.sig.asyncness.is_some();
|
||||
|
||||
// Extract parameters (excluding self)
|
||||
let params_result: Result<Vec<_>, _> = run_method
|
||||
.sig
|
||||
.inputs
|
||||
.iter()
|
||||
.filter_map(|arg| {
|
||||
if let FnArg::Typed(PatType { pat, ty, .. }) = arg {
|
||||
// Extract the parameter name
|
||||
let param_name = if let Pat::Ident(pat_ident) = &**pat {
|
||||
&pat_ident.ident
|
||||
} else {
|
||||
return Some(Err(syn::Error::new_spanned(
|
||||
pat,
|
||||
"Unsupported parameter pattern\n\n\
|
||||
Parameters must be simple identifiers like 'input: &T' or 'a: &i32'.",
|
||||
)));
|
||||
};
|
||||
|
||||
// Extract the inner type from &Type
|
||||
let inner_type = if let Type::Reference(type_ref) = &**ty {
|
||||
&type_ref.elem
|
||||
} else {
|
||||
return Some(Err(syn::Error::new_spanned(
|
||||
ty,
|
||||
"All parameters must be references (&T)\n\n\
|
||||
Task inputs must be references to allow sharing data between tasks.\n\
|
||||
Change this parameter from 'T' to '&T'.",
|
||||
)));
|
||||
};
|
||||
|
||||
Some(Ok((param_name.clone(), inner_type.clone())))
|
||||
} else {
|
||||
None // Skip self parameter
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let params = match params_result {
|
||||
Ok(p) => p,
|
||||
Err(e) => return e.to_compile_error().into(),
|
||||
};
|
||||
|
||||
// Extract return type
|
||||
let output_type = match &run_method.sig.output {
|
||||
ReturnType::Default => {
|
||||
return syn::Error::new_spanned(
|
||||
&run_method.sig,
|
||||
"run() method must have an explicit return type\n\n\
|
||||
Specify the output type: async fn run(...) -> OutputType\n\
|
||||
For tasks that don't return a value, use '-> ()'.",
|
||||
)
|
||||
.to_compile_error()
|
||||
.into();
|
||||
}
|
||||
ReturnType::Type(_, ty) => ty.clone(),
|
||||
};
|
||||
|
||||
// Build Input type based on parameter count
|
||||
let input_type = match params.len() {
|
||||
0 => quote! { () },
|
||||
1 => {
|
||||
let (_name, ty) = ¶ms[0];
|
||||
quote! { #ty }
|
||||
}
|
||||
_ => {
|
||||
let types: Vec<_> = params.iter().map(|(_, ty)| ty).collect();
|
||||
quote! { ( #(#types),* ) }
|
||||
}
|
||||
};
|
||||
|
||||
// Generate parameter destructuring for the wrapper run() method
|
||||
let (param_destructure, param_refs) = if params.is_empty() {
|
||||
(quote! { _ }, quote! {})
|
||||
} else if params.len() == 1 {
|
||||
let (name, _) = ¶ms[0];
|
||||
(quote! { #name }, quote! { &#name })
|
||||
} else {
|
||||
let names: Vec<_> = params.iter().map(|(name, _)| name).collect();
|
||||
let refs: Vec<_> = params.iter().map(|(name, _)| quote! { &#name }).collect();
|
||||
(quote! { ( #(#names),* ) }, quote! { #(#refs),* })
|
||||
};
|
||||
|
||||
// Clone the run method and rename it to run_impl
|
||||
let mut run_impl_method = run_method.clone();
|
||||
run_impl_method.sig.ident = syn::Ident::new("run_impl", run_method.sig.ident.span());
|
||||
|
||||
// Check if the method has a self receiver
|
||||
let has_self_receiver = run_method
|
||||
.sig
|
||||
.inputs
|
||||
.iter()
|
||||
.any(|arg| matches!(arg, FnArg::Receiver(_)));
|
||||
|
||||
// Generate the Task trait implementation based on whether we have self and async/sync
|
||||
let expanded = if has_self_receiver {
|
||||
// Stateful task - consumes self but delegates to a method that borrows
|
||||
if is_async {
|
||||
// Async with self
|
||||
quote! {
|
||||
impl Task for #struct_name {
|
||||
type Input = #input_type;
|
||||
type Output = #output_type;
|
||||
|
||||
async fn run(mut self, input: Self::Input) -> Self::Output {
|
||||
let #param_destructure = input;
|
||||
self.run_impl(#param_refs).await
|
||||
}
|
||||
}
|
||||
|
||||
impl #struct_name {
|
||||
#run_impl_method
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Sync with self - wrap in async block
|
||||
quote! {
|
||||
impl Task for #struct_name {
|
||||
type Input = #input_type;
|
||||
type Output = #output_type;
|
||||
|
||||
async fn run(mut self, input: Self::Input) -> Self::Output {
|
||||
let #param_destructure = input;
|
||||
self.run_impl(#param_refs)
|
||||
}
|
||||
}
|
||||
|
||||
impl #struct_name {
|
||||
#run_impl_method
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Stateless task
|
||||
if is_async {
|
||||
// Async stateless
|
||||
quote! {
|
||||
impl Task for #struct_name {
|
||||
type Input = #input_type;
|
||||
type Output = #output_type;
|
||||
|
||||
async fn run(self, input: Self::Input) -> Self::Output {
|
||||
let #param_destructure = input;
|
||||
Self::run_impl(#param_refs).await
|
||||
}
|
||||
}
|
||||
|
||||
impl #struct_name {
|
||||
#[inline]
|
||||
#run_impl_method
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Sync stateless
|
||||
quote! {
|
||||
impl Task for #struct_name {
|
||||
type Input = #input_type;
|
||||
type Output = #output_type;
|
||||
|
||||
async fn run(self, input: Self::Input) -> Self::Output {
|
||||
let #param_destructure = input;
|
||||
Self::run_impl(#param_refs)
|
||||
}
|
||||
}
|
||||
|
||||
impl #struct_name {
|
||||
#[inline]
|
||||
#run_impl_method
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
TokenStream::from(expanded)
|
||||
}
|
||||
Reference in new issue
Block a user