WorkflowValidator.java

package org.hammer.audio.workflow;

import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;

/** Structural workflow validation for the pure domain model. */
public final class WorkflowValidator {

  private static final String EDGE_PREFIX = "Edge ";
  private final TypeRegistry typeRegistry;

  public WorkflowValidator() {
    this(TypeRegistry.defaultRegistry());
  }

  public WorkflowValidator(TypeRegistry typeRegistry) {
    this.typeRegistry = Objects.requireNonNull(typeRegistry, "typeRegistry");
  }

  public List<String> validate(Workflow workflow) {
    Objects.requireNonNull(workflow, "workflow");
    Set<String> nodeIds = new LinkedHashSet<>();
    Set<String> edgeIds = new LinkedHashSet<>();
    List<String> violations = new ArrayList<>();

    for (Node node : workflow.nodes()) {
      if (!nodeIds.add(node.id())) {
        violations.add("Duplicate node id: " + node.id());
      }
      Set<String> portIds = new LinkedHashSet<>();
      validatePorts(
          node, node.inputPorts(), PortDirection.INPUT, "inputPorts", portIds, violations);
      validatePorts(
          node, node.outputPorts(), PortDirection.OUTPUT, "outputPorts", portIds, violations);
    }

    for (Edge edge : workflow.edges()) {
      if (!edgeIds.add(edge.id())) {
        violations.add("Duplicate edge id: " + edge.id());
      }
      validateEdge(edge, workflow, violations);
    }

    return List.copyOf(violations);
  }

  public boolean isValid(Workflow workflow) {
    return validate(workflow).isEmpty();
  }

  private void validatePorts(
      Node node,
      List<Port> ports,
      PortDirection expectedDirection,
      String collectionName,
      Set<String> portIds,
      List<String> violations) {
    for (Port port : ports) {
      if (port.direction() != expectedDirection) {
        violations.add(
            "Node "
                + node.id()
                + " has "
                + collectionName
                + " port "
                + port.id()
                + " with direction "
                + port.direction());
      }
      if (!portIds.add(port.id())) {
        violations.add("Node " + node.id() + " has duplicate port id: " + port.id());
      }
      if (!typeRegistry.isRegistered(port.dataType())) {
        violations.add(
            "Node "
                + node.id()
                + " has "
                + collectionName
                + " port "
                + port.id()
                + " with unknown data type "
                + port.dataType().id());
      }
    }
  }

  private void validateEdge(Edge edge, Workflow workflow, List<String> violations) {
    Node sourceNode = findNode(workflow, edge.sourceNodeId());
    if (sourceNode == null) {
      violations.add(
          EDGE_PREFIX + edge.id() + " references missing source node " + edge.sourceNodeId());
      return;
    }
    Node targetNode = findNode(workflow, edge.targetNodeId());
    if (targetNode == null) {
      violations.add(
          EDGE_PREFIX + edge.id() + " references missing target node " + edge.targetNodeId());
      return;
    }

    Port sourcePort =
        sourceNode.outputPorts().stream()
            .filter(port -> port.id().equals(edge.sourcePortId()))
            .findFirst()
            .orElse(null);
    if (sourcePort == null) {
      violations.add(
          EDGE_PREFIX
              + edge.id()
              + " references missing source port "
              + edge.sourceNodeId()
              + ":"
              + edge.sourcePortId());
    }

    Port targetPort =
        targetNode.inputPorts().stream()
            .filter(port -> port.id().equals(edge.targetPortId()))
            .findFirst()
            .orElse(null);
    if (targetPort == null) {
      violations.add(
          EDGE_PREFIX
              + edge.id()
              + " references missing target port "
              + edge.targetNodeId()
              + ":"
              + edge.targetPortId());
    }
    if (sourcePort != null
        && targetPort != null
        && !typeRegistry.areCompatible(sourcePort.dataType(), targetPort.dataType())) {
      violations.add(
          EDGE_PREFIX
              + edge.id()
              + " connects incompatible data types "
              + sourcePort.dataType().id()
              + " -> "
              + targetPort.dataType().id());
    }
  }

  private static Node findNode(Workflow workflow, String nodeId) {
    return workflow.nodes().stream()
        .filter(node -> node.id().equals(nodeId))
        .findFirst()
        .orElse(null);
  }
}