a3s-flow 1.1.0

Durable workflow engine and Rust SDK for A3S
Documentation
use serde_json::{Map, Value};
use sha2::{Digest, Sha256};

use super::{WorkflowDag, WorkflowDsl, WorkflowDslError};

const EXECUTION_DIGEST_DOMAIN: &[u8] = b"a3s.flow.workflow_dsl.execution.v1\0";
const GRAPH_EXECUTION_DIGEST_DOMAIN: &[u8] = b"a3s.flow.workflow_dag.execution.v1\0";
const NODE_PRESENTATION_FIELDS: &[&str] = &[
    "draggable",
    "height",
    "position",
    "positionAbsolute",
    "selected",
    "selectable",
    "sourcePosition",
    "targetPosition",
    "type",
    "width",
    "zIndex",
];
const NODE_DATA_PRESENTATION_FIELDS: &[&str] = &["desc", "height", "selected", "title", "width"];
const EDGE_PRESENTATION_FIELDS: &[&str] =
    &["animated", "hidden", "selected", "style", "type", "zIndex"];

pub(super) fn document_execution_digest(
    document: &WorkflowDsl,
) -> Result<String, WorkflowDslError> {
    document.graph().execution_plan()?;
    let mut value =
        serde_json::to_value(document).map_err(|error| WorkflowDslError::Serialization {
            message: error.to_string(),
        })?;
    normalize_document(&mut value)?;
    stable_digest(EXECUTION_DIGEST_DOMAIN, &value)
}

pub(super) fn graph_execution_digest(graph: &WorkflowDag) -> Result<String, WorkflowDslError> {
    graph.execution_plan()?;
    let mut value =
        serde_json::to_value(graph).map_err(|error| WorkflowDslError::Serialization {
            message: error.to_string(),
        })?;
    normalize_graph(&mut value, "graph")?;
    stable_digest(GRAPH_EXECUTION_DIGEST_DOMAIN, &value)
}

fn normalize_document(value: &mut Value) -> Result<(), WorkflowDslError> {
    let document = object_mut(value, "document")?;
    let app = document
        .get_mut("app")
        .ok_or_else(|| invalid_serialized("document.app is missing"))?;
    let mode = object_mut(app, "document.app")?
        .remove("mode")
        .ok_or_else(|| invalid_serialized("document.app.mode is missing"))?;
    document.insert(
        "app".to_owned(),
        Value::Object(Map::from_iter([("mode".into(), mode)])),
    );

    let workflow = object_mut(
        document
            .get_mut("workflow")
            .ok_or_else(|| invalid_serialized("document.workflow is missing"))?,
        "document.workflow",
    )?;
    let graph = workflow
        .get_mut("graph")
        .ok_or_else(|| invalid_serialized("document.workflow.graph is missing"))?;
    normalize_graph(graph, "document.workflow.graph")
}

fn normalize_graph(value: &mut Value, label: &str) -> Result<(), WorkflowDslError> {
    let graph = object_mut(value, label)?;
    graph.remove("viewport");

    let nodes = graph
        .get_mut("nodes")
        .and_then(Value::as_array_mut)
        .ok_or_else(|| invalid_serialized(format!("{label}.nodes is not an array")))?;
    for node in nodes.iter_mut() {
        let node = object_mut(node, "workflow DAG node")?;
        for field in NODE_PRESENTATION_FIELDS {
            node.remove(*field);
        }
        if let Some(data) = node.get_mut("data") {
            let data = object_mut(data, "workflow DAG node.data")?;
            for field in NODE_DATA_PRESENTATION_FIELDS {
                data.remove(*field);
            }
        }
    }
    sort_by_id(nodes, "workflow DAG nodes")?;

    let edges = graph
        .get_mut("edges")
        .and_then(Value::as_array_mut)
        .ok_or_else(|| invalid_serialized(format!("{label}.edges is not an array")))?;
    for edge in edges.iter_mut() {
        let edge = object_mut(edge, "workflow DAG edge")?;
        for field in EDGE_PRESENTATION_FIELDS {
            edge.remove(*field);
        }
    }
    sort_by_id(edges, "workflow DAG edges")?;
    Ok(())
}

fn stable_digest(domain: &[u8], value: &Value) -> Result<String, WorkflowDslError> {
    let encoded = serde_json::to_vec(value).map_err(|error| WorkflowDslError::Serialization {
        message: error.to_string(),
    })?;
    let mut hasher = Sha256::new();
    hasher.update(domain);
    hasher.update(encoded);
    Ok(format!("{:x}", hasher.finalize()))
}

fn sort_by_id(values: &mut [Value], label: &str) -> Result<(), WorkflowDslError> {
    for value in values.iter() {
        if value.get("id").and_then(Value::as_str).is_none() {
            return Err(invalid_serialized(format!("{label} contain an invalid ID")));
        }
    }
    values.sort_by(|left, right| {
        left.get("id")
            .and_then(Value::as_str)
            .unwrap_or_default()
            .cmp(right.get("id").and_then(Value::as_str).unwrap_or_default())
    });
    Ok(())
}

fn object_mut<'a>(
    value: &'a mut Value,
    label: &str,
) -> Result<&'a mut Map<String, Value>, WorkflowDslError> {
    value
        .as_object_mut()
        .ok_or_else(|| invalid_serialized(format!("{label} is not an object")))
}

fn invalid_serialized(message: impl Into<String>) -> WorkflowDslError {
    WorkflowDslError::Serialization {
        message: message.into(),
    }
}