Skip to main content

graph/
graph.rs

1//! Parse, compose and export metadata without opening an App or a lower service.
2//! Run with `cargo run -p app-kit --example graph`.
3
4use app_kit::AppError;
5use app_kit::workflow::{
6    GraphComposition, GraphConnection, GraphFormat, GraphImport, WorkflowDefinition,
7};
8use serde_json::json;
9
10fn main() -> Result<(), AppError> {
11    let mut imports = Vec::new();
12    for (id, prefix) in [("first-counter", "first_"), ("second-counter", "second_")] {
13        let definition = WorkflowDefinition::parse(
14            &json!({
15                "schema":"app-kit.workflow/v2", "id":id, "revision":"r1",
16                "state":{"count":0}, "required":["count"],
17                "reducers":{"count":"reject_concurrent"}, "entry":"add",
18                "nodes":[{"id":"add", "name":"Add one", "step":{
19                    "kind":"state", "mutations":[{
20                        "kind":"increment", "target":"count",
21                        "value":{"kind":"literal", "value":1}
22                    }]
23                }}],
24                "routes":{}
25            })
26            .to_string(),
27        )?;
28        imports.push(GraphImport {
29            definition,
30            prefix: prefix.into(),
31        });
32    }
33    let definition = WorkflowDefinition::compose(GraphComposition {
34        id: "two-counters".parse()?,
35        revision: "r1".into(),
36        entry: "first_add".into(),
37        imports,
38        connections: vec![GraphConnection {
39            from: "first_add".into(),
40            to: "second_add".into(),
41        }],
42    })?;
43    let graph = definition.graph();
44    assert_eq!(graph.entry, "first_add");
45    assert_eq!(graph.nodes.len(), 2);
46    assert_eq!(graph.nodes[0].id, "first_add");
47    assert_eq!(graph.nodes[1].id, "second_add");
48    assert!(
49        graph
50            .edges
51            .iter()
52            .any(|edge| { edge.from == "first_add" && edge.to.as_deref() == Some("second_add") })
53    );
54
55    let inspection = definition.export(GraphFormat::Json)?;
56    let inspection: serde_json::Value = serde_json::from_str(&inspection)
57        .map_err(|_| AppError::Integration("example graph JSON was invalid".into()))?;
58    assert_eq!(inspection["nodes"].as_array().map(Vec::len), Some(2));
59    assert!(definition.export(GraphFormat::Dot)?.starts_with("digraph "));
60    println!("{}", definition.export(GraphFormat::Mermaid)?);
61
62    // Built-in nodes retain their canonical authored source. Tracking requires
63    // actual source for every node and is a separate content-export format.
64    let tracking = graph.tracking_record()?;
65    assert_eq!(tracking["type"], "application");
66    assert_eq!(tracking["actions"].as_array().map(Vec::len), Some(2));
67    Ok(())
68}