WorkflowSessionApiModels.java

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

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.PositiveOrZero;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.hammer.audio.workflow.Workflow;
import org.hammer.audio.workflow.collaboration.CollaborationMode;
import org.hammer.audio.workflow.collaboration.OperationActor;
import org.hammer.audio.workflow.collaboration.PresenceState;
import org.hammer.audio.workflow.collaboration.RedoWorkflowCommand;
import org.hammer.audio.workflow.collaboration.UndoWorkflowCommand;
import org.hammer.audio.workflow.collaboration.WorkflowHistoryCommandResult;
import org.hammer.audio.workflow.collaboration.WorkflowSessionEvent;
import org.hammer.audio.workflow.collaboration.WorkflowSessionRegistry.SessionSnapshot;
import org.hammer.audio.workflow.collaboration.WorkflowUndoPreview;
import org.hammer.audio.workflow.collaboration.store.WorkflowOperationCommandMetadata.Kind;
import org.hammer.audio.workflow.editor.WorkflowProjection;
import tools.jackson.databind.JsonNode;

/** Request and response models for the workflow-session REST API. */
public final class WorkflowSessionApiModels {

  private static final String ACTOR_FIELD = "actor";

  private WorkflowSessionApiModels() {
    // Utility class.
  }

  static String defaultWorkflowId(String sessionId) {
    return "workflow." + sessionId;
  }

  static String defaultWorkflowName(String sessionId) {
    return "Workflow " + sessionId;
  }

  /**
   * Actor identity supplied by the transport/authentication boundary.
   *
   * @param actorId stable actor identifier within collaboration history
   * @param userId stable authenticated user identifier
   * @param displayName human-readable actor name
   */
  public record ActorRequest(
      @NotBlank String actorId, @NotBlank String userId, @NotBlank String displayName) {
    OperationActor toDomain() {
      return new OperationActor(actorId, userId, displayName);
    }
  }

  /**
   * Request for creating and initially joining a session.
   *
   * @param sessionId stable collaboration-session identifier
   * @param mode immutable collaboration mode
   * @param actor owner and initially joined actor
   * @param workflowId optional initial workflow identifier
   * @param workflowName optional initial workflow display name
   */
  public record CreateSessionRequest(
      @NotBlank String sessionId,
      @NotNull CollaborationMode mode,
      @Valid @NotNull ActorRequest actor,
      String workflowId,
      String workflowName) {
    Workflow initialWorkflow() {
      String resolvedWorkflowId =
          workflowId == null || workflowId.isBlank() ? defaultWorkflowId(sessionId) : workflowId;
      String resolvedWorkflowName =
          workflowName == null || workflowName.isBlank()
              ? defaultWorkflowName(sessionId)
              : workflowName;
      return new Workflow(resolvedWorkflowId, resolvedWorkflowName, List.of(), List.of());
    }
  }

  /**
   * Request for joining an existing session.
   *
   * @param actorId stable actor identifier within collaboration history
   * @param userId stable authenticated user identifier
   * @param displayName human-readable actor name
   */
  public record JoinSessionRequest(
      @NotBlank String actorId, @NotBlank String userId, @NotBlank String displayName) {
    OperationActor toDomain() {
      return new OperationActor(actorId, userId, displayName);
    }
  }

  /**
   * Request for leaving or closing a session.
   *
   * @param actorId actor requesting the lifecycle transition
   */
  public record ActorIdRequest(@NotBlank String actorId) {
    public ActorIdRequest {
      actorId = Objects.requireNonNull(actorId, "actorId");
    }
  }

  /**
   * Request for one server-authoritative semantic session operation.
   *
   * @param mode collaboration mode expected by the client
   * @param actor actor authoring the operation
   * @param expectedRevision optional optimistic-concurrency revision
   * @param operation transport representation of the semantic operation
   */
  public record SessionOperationRequest(
      @NotNull CollaborationMode mode,
      @Valid @NotNull ActorRequest actor,
      @PositiveOrZero Long expectedRevision,
      @NotNull JsonNode operation) {
    public SessionOperationRequest {
      Objects.requireNonNull(mode, "mode");
      Objects.requireNonNull(actor, ACTOR_FIELD);
      Objects.requireNonNull(operation, "operation");
    }
  }

  /**
   * Request for a revision-bound undo preview.
   *
   * @param actor actor requesting the preview
   * @param targetOperationId optional explicit target, required in shared-undo mode
   */
  public record UndoPreviewRequest(@Valid @NotNull ActorRequest actor, String targetOperationId) {
    public UndoPreviewRequest {
      Objects.requireNonNull(actor, ACTOR_FIELD);
    }
  }

  /**
   * Request for an idempotent semantic undo command.
   *
   * @param commandId stable idempotency identity owned by the actor
   * @param actor actor requesting the undo
   * @param expectedRevision optimistic-concurrency revision
   * @param targetOperationId optional explicit operation target
   * @param previewId required revision-bound preview identity in shared-undo mode
   */
  public record UndoCommandRequest(
      @NotBlank String commandId,
      @Valid @NotNull ActorRequest actor,
      @PositiveOrZero long expectedRevision,
      String targetOperationId,
      String previewId) {
    public UndoCommandRequest {
      Objects.requireNonNull(actor, ACTOR_FIELD);
    }

    UndoWorkflowCommand toDomain() {
      return new UndoWorkflowCommand(
          commandId, actor.toDomain(), expectedRevision, targetOperationId, previewId);
    }
  }

  /**
   * Request for an idempotent semantic redo command.
   *
   * @param commandId stable idempotency identity owned by the actor
   * @param actor actor requesting the redo
   * @param expectedRevision optimistic-concurrency revision
   * @param targetUndoOperationId accepted undo operation to invert
   */
  public record RedoCommandRequest(
      @NotBlank String commandId,
      @Valid @NotNull ActorRequest actor,
      @PositiveOrZero long expectedRevision,
      @NotBlank String targetUndoOperationId) {
    public RedoCommandRequest {
      Objects.requireNonNull(actor, ACTOR_FIELD);
    }

    RedoWorkflowCommand toDomain() {
      return new RedoWorkflowCommand(
          commandId, actor.toDomain(), expectedRevision, targetUndoOperationId);
    }
  }

  /**
   * Stable undo preview response.
   *
   * @param previewId identity bound to session revision, target, and blockers
   * @param targetOperationId selected accepted operation
   * @param targetActorId actor that authored the selected operation
   * @param operationType semantic operation type
   * @param affectedObjectIds semantic objects affected by the operation
   * @param revision revision at which the preview is valid
   * @param safe whether the inverse currently has no blockers
   * @param blockingOperations later operations that conflict with the inverse
   */
  public record UndoPreviewResponse(
      String previewId,
      String targetOperationId,
      String targetActorId,
      String operationType,
      List<String> affectedObjectIds,
      long revision,
      boolean safe,
      List<BlockingOperationResponse> blockingOperations) {
    public UndoPreviewResponse {
      affectedObjectIds = List.copyOf(affectedObjectIds);
      blockingOperations = List.copyOf(blockingOperations);
    }

    static UndoPreviewResponse from(WorkflowUndoPreview preview) {
      return new UndoPreviewResponse(
          preview.previewId(),
          preview.targetOperationId(),
          preview.targetActorId(),
          preview.operationType(),
          preview.affectedObjectIds(),
          preview.revision(),
          preview.safe(),
          preview.blockingOperations().stream().map(BlockingOperationResponse::from).toList());
    }
  }

  /**
   * Machine-readable later operation blocking undo or redo.
   *
   * @param operationId stable accepted-operation identifier
   * @param actorId actor that authored the blocking operation
   * @param conflictingObjectIds semantic object identifiers shared with the target
   */
  public record BlockingOperationResponse(
      String operationId, String actorId, List<String> conflictingObjectIds) {
    public BlockingOperationResponse {
      conflictingObjectIds = List.copyOf(conflictingObjectIds);
    }

    static BlockingOperationResponse from(WorkflowUndoPreview.BlockingOperation blocker) {
      return new BlockingOperationResponse(
          blocker.operationId(), blocker.actorId(), blocker.conflictingObjectIds());
    }
  }

  /**
   * Stable response for accepted undo and redo commands.
   *
   * @param projection resulting canonical workflow projection
   * @param commandKind accepted command kind
   * @param commandId stable command idempotency identity
   * @param targetOperationId operation targeted by the command
   * @param operationId newly appended inverse-operation identifier
   * @param revision resulting semantic revision
   * @param sequence resulting ordered event sequence
   */
  public record HistoryCommandResponse(
      WorkflowProjection projection,
      Kind commandKind,
      String commandId,
      String targetOperationId,
      String operationId,
      long revision,
      long sequence) {
    static HistoryCommandResponse from(WorkflowHistoryCommandResult result) {
      return new HistoryCommandResponse(
          WorkflowProjection.fromWorkflow(result.workflow()),
          result.command().kind(),
          result.command().commandId(),
          result.command().targetOperationId(),
          result.operationId(),
          result.revision(),
          result.sequence());
    }
  }

  /**
   * Request for non-semantic cursor, selection, or viewport presence state.
   *
   * @param actor actor publishing the state
   * @param observedAt client observation time, or {@code null} to use server time
   * @param attributes transport-neutral presence attributes
   */
  public record PresenceRequest(
      @Valid @NotNull ActorRequest actor,
      Instant observedAt,
      @NotNull Map<String, String> attributes) {
    public PresenceRequest {
      Objects.requireNonNull(actor, ACTOR_FIELD);
      attributes = Map.copyOf(Objects.requireNonNull(attributes, "attributes"));
    }

    PresenceState toDomain() {
      Instant timestamp = observedAt == null ? Instant.now() : observedAt;
      return new PresenceState(actor.actorId(), timestamp, attributes);
    }
  }

  /**
   * Stable transport response for accepted presence state.
   *
   * @param actorId actor represented by the state
   * @param observedAt accepted observation timestamp
   * @param attributes transport-neutral presence attributes
   */
  public record PresenceResponse(
      String actorId, Instant observedAt, Map<String, String> attributes) {
    public PresenceResponse {
      attributes = Map.copyOf(Objects.requireNonNull(attributes, "attributes"));
    }

    static PresenceResponse from(PresenceState state) {
      return new PresenceResponse(state.actorId(), state.observedAt(), state.attributes());
    }
  }

  /**
   * Ordered SSE payload derived from a transport-neutral session event.
   *
   * @param eventId stable event identifier
   * @param sessionId owning collaboration-session identifier
   * @param sequence monotonically increasing event sequence
   * @param revision semantic workflow revision
   * @param occurredAt event occurrence timestamp
   * @param type event type
   * @param actor optional actor associated with the event
   * @param operationId optional accepted semantic operation identifier
   * @param projection optional canonical workflow projection
   * @param attributes transport-neutral event attributes
   */
  public record SessionEventResponse(
      String eventId,
      String sessionId,
      long sequence,
      long revision,
      Instant occurredAt,
      WorkflowSessionEvent.Type type,
      ActorResponse actor,
      String operationId,
      WorkflowProjection projection,
      Map<String, String> attributes) {
    public SessionEventResponse {
      attributes = Map.copyOf(Objects.requireNonNull(attributes, "attributes"));
    }

    static SessionEventResponse from(WorkflowSessionEvent event) {
      ActorResponse actorResponse =
          event.actor() == null ? null : ActorResponse.from(event.actor());
      WorkflowProjection workflowProjection =
          event.workflow() == null ? null : WorkflowProjection.fromWorkflow(event.workflow());
      return new SessionEventResponse(
          event.eventId(),
          event.sessionId(),
          event.sequence(),
          event.revision(),
          event.occurredAt(),
          event.type(),
          actorResponse,
          event.operationId(),
          workflowProjection,
          event.attributes());
    }
  }

  /**
   * Stable actor representation returned by the API.
   *
   * @param actorId stable collaboration actor identifier
   * @param userId stable authenticated user identifier
   * @param displayName human-readable actor name
   */
  public record ActorResponse(String actorId, String userId, String displayName) {
    static ActorResponse from(OperationActor actor) {
      return new ActorResponse(actor.actorId(), actor.userId(), actor.displayName());
    }
  }

  /**
   * Transport response for collaboration-session metadata.
   *
   * @param sessionId stable collaboration-session identifier
   * @param mode immutable collaboration mode
   * @param owner session owner
   * @param createdAt session creation timestamp
   * @param participants currently joined actors
   * @param operationCount number of accepted semantic operations
   * @param workflowId canonical workflow identifier
   * @param revision current semantic revision
   * @param sequence current ordered event sequence
   */
  public record SessionResponse(
      String sessionId,
      CollaborationMode mode,
      ActorResponse owner,
      Instant createdAt,
      List<ActorResponse> participants,
      int operationCount,
      String workflowId,
      long revision,
      long sequence) {
    public SessionResponse {
      participants = List.copyOf(Objects.requireNonNull(participants, "participants"));
    }

    static SessionResponse from(SessionSnapshot snapshot) {
      return new SessionResponse(
          snapshot.sessionId(),
          snapshot.mode(),
          ActorResponse.from(snapshot.owner()),
          snapshot.createdAt(),
          snapshot.participants().stream().map(ActorResponse::from).toList(),
          snapshot.operationCount(),
          snapshot.workflowId(),
          snapshot.revision(),
          snapshot.sequence());
    }
  }
}