WorkflowOperationHttpParser.java

package org.hammer.audio.workflow.editor.http;

import java.time.Instant;
import java.util.Arrays;
import org.hammer.audio.workflow.Edge;
import org.hammer.audio.workflow.Node;
import org.hammer.audio.workflow.WorkflowOperation;
import org.hammer.audio.workflow.catalog.ExperimentNodeCatalog;
import tools.jackson.databind.JsonNode;

/** Parses transport JSON into semantic workflow operations shared by editor/session endpoints. */
final class WorkflowOperationHttpParser {

  private static final String JSON_FIELD_TIMESTAMP = "timestamp";

  private WorkflowOperationHttpParser() {
    throw new AssertionError("No instances");
  }

  static WorkflowOperation parse(JsonNode json, String defaultAuthor) {
    String type = requiredText(json, "type");
    String operationId = requiredText(json, "operationId");
    String author = json.has("author") ? json.get("author").asText() : defaultAuthor;
    Instant timestamp =
        json.has(JSON_FIELD_TIMESTAMP)
            ? Instant.parse(json.get(JSON_FIELD_TIMESTAMP).asText())
            : Instant.now();
    return switch (type) {
      case "CreateNode" -> {
        String nodeId = requiredText(json, "nodeId");
        String catalogType = requiredText(json, "catalogType");
        Node node = createCatalogNode(catalogType, nodeId);
        yield new WorkflowOperation.CreateNode(operationId, timestamp, author, node);
      }
      case "ConnectPorts" -> {
        JsonNode edgeJson = requireObject(json, "edge");
        Edge edge = edge(edgeJson);
        yield new WorkflowOperation.ConnectPorts(operationId, timestamp, author, edge);
      }
      case "DisconnectPorts" -> {
        String edgeId = requiredText(json, "edgeId");
        Edge disconnectedEdge = edge(requireObject(json, "disconnectedEdge"));
        yield new WorkflowOperation.DisconnectPorts(
            operationId, timestamp, author, edgeId, disconnectedEdge);
      }
      case "UpdateProperty" -> {
        String target = requiredText(json, "target");
        String targetId = requiredText(json, "targetId");
        String propertyKey = requiredText(json, "propertyKey");
        String newValue = json.has("newValue") ? json.get("newValue").asText() : null;
        String previousValue =
            json.has("previousValue") ? json.get("previousValue").asText() : null;
        WorkflowOperation.PropertyTarget propertyTarget = parsePropertyTarget(target);
        yield new WorkflowOperation.UpdateProperty(
            operationId,
            timestamp,
            author,
            propertyTarget,
            targetId,
            propertyKey,
            previousValue,
            newValue);
      }
      default -> throw new IllegalArgumentException("Unknown operation type: " + type);
    };
  }

  private static Edge edge(JsonNode edgeJson) {
    return new Edge(
        requiredText(edgeJson, "id"),
        requiredText(edgeJson, "sourceNodeId"),
        requiredText(edgeJson, "sourcePortId"),
        requiredText(edgeJson, "targetNodeId"),
        requiredText(edgeJson, "targetPortId"));
  }

  private static WorkflowOperation.PropertyTarget parsePropertyTarget(String target) {
    return Arrays.stream(WorkflowOperation.PropertyTarget.values())
        .filter(value -> value.name().equals(target))
        .findFirst()
        .orElseThrow(() -> new IllegalArgumentException("Unknown PropertyTarget: " + target));
  }

  private static Node createCatalogNode(String catalogType, String nodeId) {
    return switch (catalogType) {
      case "recording-input" -> ExperimentNodeCatalog.recordingInput(nodeId);
      case "synthetic-signal-generator" -> ExperimentNodeCatalog.syntheticSignalGenerator(nodeId);
      case "humbug-db-import" -> ExperimentNodeCatalog.humBugDbImport(nodeId);
      case "gain" -> ExperimentNodeCatalog.gain(nodeId);
      case "bandpass-filter" -> ExperimentNodeCatalog.bandpassFilter(nodeId);
      case "fft" -> ExperimentNodeCatalog.fft(nodeId);
      case "wingbeat-feature-extraction" -> ExperimentNodeCatalog.wingbeatFeatureExtraction(nodeId);
      case "classifier" -> ExperimentNodeCatalog.classifier(nodeId);
      case "localization" -> ExperimentNodeCatalog.localization(nodeId);
      case "benchmark" -> ExperimentNodeCatalog.benchmark(nodeId);
      case "report" -> ExperimentNodeCatalog.report(nodeId);
      case "evidence-export" -> ExperimentNodeCatalog.evidenceExport(nodeId);
      default -> throw new IllegalArgumentException("Unknown catalog node type: " + catalogType);
    };
  }

  private static JsonNode requireObject(JsonNode node, String field) {
    JsonNode value = node.get(field);
    if (value == null || !value.isObject()) {
      throw new IllegalArgumentException("Missing required object field: " + field);
    }
    return value;
  }

  private static String requiredText(JsonNode node, String field) {
    JsonNode value = node.get(field);
    if (value == null || value.isNull() || value.asText().isBlank()) {
      throw new IllegalArgumentException("Missing required field: " + field);
    }
    return value.asText();
  }
}