Back to Delivery 2 Reusable Quality Utilities

delivery-02-reusable-quality-utilities/src/validation.ts

import type { QualityEnvironment, QualityExecutionResult } from "./models.js";

const qualityEnvironments = new Set<QualityEnvironment>(["development", "test", "staging"]);

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

function readNonEmptyString(record: Record<string, unknown>, key: string): string {
  const value = record[key];
  if (typeof value !== "string" || value.trim().length === 0) {
    throw new Error(`Expected a non-empty string for ${key}.`);
  }

  return value;
}

function readFiniteNumber(record: Record<string, unknown>, key: string): number {
  const value = record[key];
  if (typeof value !== "number" || !Number.isFinite(value)) {
    throw new Error(`Expected a finite number for ${key}.`);
  }

  return value;
}

function readBoolean(record: Record<string, unknown>, key: string): boolean {
  const value = record[key];
  if (typeof value !== "boolean") {
    throw new Error(`Expected a boolean for ${key}.`);
  }

  return value;
}

function readQualityEnvironment(record: Record<string, unknown>): QualityEnvironment {
  const environment = readNonEmptyString(record, "environment").trim();
  if (!qualityEnvironments.has(environment as QualityEnvironment)) {
    throw new Error(`Unexpected environment: ${environment}.`);
  }

  return environment as QualityEnvironment;
}

/**
 * Converts one untrusted runtime value into the trusted model used by the
 * remaining modules. It does not parse JSON or read a file; those are outer
 * boundary responsibilities.
 */
export function parseQualityExecutionResult(input: unknown): QualityExecutionResult {
  if (!isRecord(input)) {
    throw new Error("Expected a quality execution result object.");
  }

  const result: QualityExecutionResult = {
    executionId: readNonEmptyString(input, "executionId"),
    endpoint: readNonEmptyString(input, "endpoint"),
    statusCode: readFiniteNumber(input, "statusCode"),
    responseTimeMs: readFiniteNumber(input, "responseTimeMs"),
    environment: readQualityEnvironment(input),
    validationPassed: readBoolean(input, "validationPassed"),
  };

  if (!Number.isInteger(result.statusCode) || result.statusCode < 100 || result.statusCode > 599) {
    throw new Error("statusCode must be an integer between 100 and 599.");
  }

  if (result.responseTimeMs < 0) {
    throw new Error("responseTimeMs cannot be negative.");
  }

  return result;
}

/**
 * Uses an all-or-nothing policy: one malformed record prevents the caller
 * from receiving a partial collection that might be mistaken for complete
 * evidence.
 */
export function parseQualityExecutionResults(input: unknown): QualityExecutionResult[] {
  if (!Array.isArray(input)) {
    throw new Error("Expected an array of quality execution results.");
  }

  return input.map((value, index) => {
    try {
      return parseQualityExecutionResult(value);
    } catch (error: unknown) {
      const reason = error instanceof Error ? error.message : "Unknown validation error.";
      throw new Error(`Invalid quality execution result at index ${index}: ${reason}`);
    }
  });
}