operese-dagx 0.4.1

A minimal, type-safe, runtime-agnostic async DAG (Directed Acyclic Graph) executor with compile-time cycle prevention and true parallel execution
Documentation
//! # Getting Started: Your First DAG
//!
//! This example introduces the fundamentals of building and executing a DAG with dagx.
//!
//! ## What You'll Learn
//! - How to define tasks using the `#[task]` macro
//! - How to create a DAG and add tasks
//! - How to wire dependencies between tasks
//! - How to execute the DAG and retrieve results
//!
//! ## Key Concepts
//! - **Task**: A unit of work with typed inputs and outputs
//! - **DAG (Directed Acyclic Graph)**: A workflow where tasks depend on other tasks
//! - **TaskHandle**: A typed reference to a task's output
//! - **Dependencies**: Relationships between tasks that determine execution order
//!
//! ## Running This Example
//! ```bash
//! cargo run --example basic
//! ```
//!
//! ## Expected Output
//! ```text
//! Running DAG...
//!
//! Computing value: 2
//! Computing value: 3
//! Sum: Computing 2 + 3
//!
//! Result: 5
//! ```

use operese_dagx::{task, DagRunner};

// Step 1: Define a source task (no dependencies)
//
// Source tasks produce initial data for the DAG.
// They don't depend on any other tasks.
struct Value {
    value: i32,
}

impl Value {
    fn new(value: i32) -> Self {
        Self { value }
    }
}

// The #[task] macro converts your implementation into a full Task trait impl
#[task]
impl Value {
    // Use `&mut self` to access the task's state
    async fn run(&mut self) -> i32 {
        println!("Computing value: {}", self.value);
        self.value
    }
}

// Step 2: Define a task with dependencies
//
// This task takes TWO inputs (a and b), both of type i32.
// The order of parameters matches the order in .depends_on()
struct Add {
    label: String,
}

#[task]
impl Add {
    // Parameters after `&mut self` are dependencies
    // They're provided as references (&i32, not i32)
    async fn run(&mut self, a: &i32, b: &i32) -> i32 {
        println!("{}: Computing {} + {}", self.label, a, b);
        a + b
    }
}

#[tokio::main]
async fn main() {
    // Step 3: Create a new DAG
    let mut dag = DagRunner::new();

    // Step 4: Add source tasks (tasks with no dependencies)
    //
    // add_task() returns a TaskHandle<T> where T is the task's output type
    let x = dag.add_task(Value::new(2)); // TaskHandle<i32>
    let y = dag.add_task(Value::new(3)); // TaskHandle<i32>

    // Step 5: Add a task with dependencies
    //
    // The .depends_on() method wires up dependencies.
    // Order matters! The first handle maps to the first parameter, etc.
    let sum = dag
        .add_task(Add {
            label: "Sum".to_string(),
        })
        .depends_on((&x, &y)); // Pass handles as a tuple
                               // sum is now TaskHandle<i32>

    // Step 6: Execute the DAG
    //
    // The run() method needs a "spawner" - a function that spawns futures.
    // This enables runtime-agnostic execution.
    //
    // Execution order:
    // 1. Tasks with no dependencies run first (x and y in parallel)
    // 2. Once x and y complete, sum runs (it depends on both)
    println!("Running DAG...\n");
    let mut output = dag
        .run(|fut| async move { tokio::spawn(fut).await.unwrap() })
        .await
        .unwrap();

    // Step 7: Retrieve results
    //
    // Use .get() with a TaskHandle to retrieve the output.
    // Results are not cloned, so you can only retrieve them once.
    println!("\nResult: {}", output.get(sum));

    // Note: You can also retrieve intermediate results
    assert_eq!(output.get(x), 2);
    assert_eq!(output.get(y), 3);
}