Update all documentation to reflect that custom types work automatically with the #[task] macro, requiring only Clone + Send + Sync + 'static. Documentation updates: - README: Add custom types to features, new Custom Types section - CHANGELOG: Document custom type improvement in [Unreleased] - src/lib.rs: Add custom types example to main API docs - src/task.rs: Update Task trait docs to mention macro-generated extraction - src/extract.rs: Clarify ExtractInput is now internal-only (for task_fn) - dagx-macros: Update macro docs to emphasize automatic type handling New example: - examples/custom_types.rs: Simple 120-line example showing User type flowing through DAG with fan-out pattern (one output → two consumers) demonstrating Arc wrapping is automatic and transparent This makes the custom type capability discoverable and prominent for users, turning a previous limitation into a headline feature.
171 lines
4.8 KiB
Rust
171 lines
4.8 KiB
Rust
//! Task trait and implementations.
|
|
//!
|
|
//! This module defines the core Task trait that all DAG nodes must implement,
|
|
//! along with convenience functions for creating tasks from closures.
|
|
|
|
use std::future::Future;
|
|
|
|
/// A unit of async work with typed inputs and outputs.
|
|
///
|
|
/// Use the [`task`](crate::task) macro to implement this trait. The macro automatically
|
|
/// derives `Input` and `Output` types from your `run()` method signature and generates
|
|
/// type-specific extraction logic in `extract_and_run()`.
|
|
///
|
|
/// **Custom types work automatically!** As long as your output type implements
|
|
/// `Clone + Send + Sync + 'static`, the macro handles everything. No trait implementations needed!
|
|
///
|
|
/// # Task Patterns
|
|
///
|
|
/// dagx supports three main task patterns:
|
|
///
|
|
/// ## 1. Stateless Tasks (Pure Functions)
|
|
///
|
|
/// Tasks with no internal state - just transform inputs to outputs:
|
|
///
|
|
/// ```
|
|
/// use dagx::{task, Task};
|
|
///
|
|
/// struct Add;
|
|
///
|
|
/// #[task]
|
|
/// impl Add {
|
|
/// async fn run(a: &i32, b: &i32) -> i32 {
|
|
/// a + b
|
|
/// }
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// ## 2. Stateful Tasks with Shared State (`&self`)
|
|
///
|
|
/// Tasks that read from internal state but don't modify it:
|
|
///
|
|
/// ```
|
|
/// use dagx::{task, Task};
|
|
///
|
|
/// struct Multiplier {
|
|
/// factor: i32,
|
|
/// }
|
|
///
|
|
/// #[task]
|
|
/// impl Multiplier {
|
|
/// async fn run(&self, input: &i32) -> i32 {
|
|
/// input * self.factor
|
|
/// }
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// ## 3. Stateful Tasks with Mutable State (`&mut self`)
|
|
///
|
|
/// Tasks that modify internal state during execution:
|
|
///
|
|
/// ```
|
|
/// use dagx::{task, Task};
|
|
///
|
|
/// struct Counter {
|
|
/// count: i32,
|
|
/// }
|
|
///
|
|
/// #[task]
|
|
/// impl Counter {
|
|
/// async fn run(&mut self, increment: &i32) -> i32 {
|
|
/// self.count += increment;
|
|
/// self.count
|
|
/// }
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// # Input Patterns
|
|
///
|
|
/// - `()`: No dependencies (source task)
|
|
/// - `&T`: Single dependency
|
|
/// - `&A, &B, &C, ...`: Multiple dependencies (up to 8, order matters)
|
|
///
|
|
/// # The Underlying Trait
|
|
///
|
|
/// The `#[task]` macro generates an implementation of this trait:
|
|
///
|
|
/// # Output Wrapping
|
|
///
|
|
/// **Task outputs are automatically wrapped in `Arc<T>` internally** for efficient fan-out.
|
|
/// You define `Output = T`, but the framework wraps it in `Arc<T>` before sending to channels.
|
|
/// This enables O(1) sharing for 1:N dependencies - Arc is cloned (cheap), not your data.
|
|
///
|
|
/// Downstream tasks receive `&T` after ExtractInput extracts from the Arc. This is transparent
|
|
/// to your task code - you work with `T`, the framework handles Arc wrapping/unwrapping.
|
|
pub trait Task: Send {
|
|
type Input: Send;
|
|
type Output: Send + Sync; // Sync required for Arc-wrapping and cross-thread sharing
|
|
|
|
fn run(self, input: Self::Input) -> impl Future<Output = Self::Output> + Send;
|
|
|
|
/// Internal method: Extract input from type-erased channels and execute the task.
|
|
///
|
|
/// This method is implemented automatically by the #[task] macro with inline
|
|
/// extraction logic based on the task's parameter signature. This allows ANY
|
|
/// type to work without requiring trait implementations.
|
|
#[doc(hidden)]
|
|
fn extract_and_run(
|
|
self,
|
|
receivers: Vec<Box<dyn std::any::Any + Send>>,
|
|
) -> impl Future<Output = Result<Self::Output, String>> + Send;
|
|
}
|
|
|
|
/// Convenience function to create a task from a closure.
|
|
///
|
|
/// **INTERNAL USE ONLY**: This function is for testing purposes only and should
|
|
/// not be used in production code. Use the `#[task]` macro instead.
|
|
#[doc(hidden)]
|
|
pub fn task_fn<I, O, Fut, F>(f: F) -> impl Task<Input = I, Output = O>
|
|
where
|
|
F: FnMut(I) -> Fut + Send + 'static,
|
|
Fut: Future<Output = O> + Send + 'static,
|
|
I: Send + crate::extract::ExtractInput + 'static,
|
|
O: Send + Sync + 'static,
|
|
{
|
|
struct TaskFn<I, O, Fut, F>
|
|
where
|
|
F: FnMut(I) -> Fut + Send,
|
|
Fut: Future<Output = O> + Send,
|
|
I: Send,
|
|
O: Send,
|
|
{
|
|
f: F,
|
|
_phantom: std::marker::PhantomData<fn(I) -> O>,
|
|
}
|
|
|
|
impl<I, O, Fut, F> Task for TaskFn<I, O, Fut, F>
|
|
where
|
|
F: FnMut(I) -> Fut + Send,
|
|
Fut: Future<Output = O> + Send,
|
|
I: Send + crate::extract::ExtractInput,
|
|
O: Send + Sync,
|
|
{
|
|
type Input = I;
|
|
type Output = O;
|
|
|
|
async fn run(mut self, input: I) -> O {
|
|
(self.f)(input).await
|
|
}
|
|
|
|
async fn extract_and_run(
|
|
self,
|
|
receivers: Vec<Box<dyn std::any::Any + Send>>,
|
|
) -> Result<Self::Output, String> {
|
|
// For task_fn, we use ExtractInput since we don't have macro-generated extraction
|
|
let input = I::extract_from_channels(receivers)
|
|
.await
|
|
.map_err(|e| format!("Failed to extract input: {}", e))?;
|
|
|
|
Ok(self.run(input).await)
|
|
}
|
|
}
|
|
|
|
TaskFn {
|
|
f,
|
|
_phantom: std::marker::PhantomData,
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests;
|