Source: src/js/common/ValidationUtilities.js

"use strict";

define(["common/ValueUtilities", "common/DateUtilities"], (
  ValueUtilities,
  DateUtilities,
) => {
  const { isNonEmptyString, isNonNegativeInteger } = ValueUtilities;
  /**
   * Shared helpers for working with simple validation error objects.
   * @namespace ValidationUtilities
   * @classcategory Common
   * @since 2.39.0
   */
  const ValidationUtilities = {
    /**
     * Validate that a string is a DOI.
     * @param {string} identifier Candidate DOI string.
     * @returns {boolean} True when the identifier matches the DOI pattern.
     */
    isValidDOI(identifier) {
      const doiRGEX =
        /^\s*(http:\/\/|https:\/\/)?(doi.org\/|dx.doi.org\/)?(doi: ?|DOI: ?)?(10\.\d{4,}(\.\d)*)\/(\w+).*$/i;

      return doiRGEX.test(identifier);
    },

    /**
     * Create a validation error object.
     * @param {string} field Field path.
     * @param {string} message Error message.
     * @param {object} [extra] Additional fields to include on the error.
     * @returns {{field:string, message:string}} Validation error.
     */
    createValidationError(field, message, extra = {}) {
      return { field, message, ...extra };
    },

    /**
     * Create a structured validation issue with optional metadata such as
     * severity, code, and field name.
     * @param {object} [options] Validation issue options.
     * @param {string|null} [options.field] Field path related to the issue.
     * @param {string} [options.message] Human-readable issue message.
     * @param {string} [options.severity] Severity label.
     * @param {string|null} [options.code] Stable issue code.
     * @returns {object} Validation issue object.
     */
    createValidationIssue({
      field = null,
      message = "",
      severity = "error",
      code = null,
      ...extra
    } = {}) {
      return {
        field,
        message,
        severity,
        code,
        ...extra,
      };
    },

    /**
     * Create a structured validation warning.
     * @param {object} [options] Validation warning options.
     * @returns {object} Validation warning object.
     */
    createValidationWarning(options = {}) {
      return ValidationUtilities.createValidationIssue({
        severity: "warning",
        ...options,
      });
    },

    /**
     * Append one parse warning in the shared validation-error shape.
     * @param {Array<object>} parseWarnings Target parse-warning array.
     * @param {string} field Field path.
     * @param {string} message Warning message.
     * @param {object} [extra] Additional warning fields.
     * @returns {object|null} The warning that was pushed, or null when the
     * target is not an array.
     */
    addParseWarning(parseWarnings, field, message, extra = {}) {
      if (!Array.isArray(parseWarnings)) return null;
      const warning = ValidationUtilities.createValidationError(
        field,
        message,
        extra,
      );
      parseWarnings.push(warning);
      return warning;
    },

    /**
     * Return a parsed value when valid, otherwise push one parse warning and
     * fall back to a safe replacement.
     * @param {object} options Salvage options.
     * @param {*} options.value Parsed candidate value.
     * @param {string} options.path Field path used in warnings and validation.
     * @param {Array<object>} [options.parseWarnings] Lossy parse warnings.
     * @param {string} options.invalidMessage Warning message for invalid input.
     * @param {Function} options.validate Validation callback that returns
     * issues.
     * @param {Function} options.fallback Fallback callback that returns the
     * replacement value.
     * @returns {*} The original value when valid, otherwise the fallback value.
     */
    fixParsedValue({
      value,
      path,
      parseWarnings = [],
      invalidMessage,
      validate,
      fallback,
    } = {}) {
      const issues = typeof validate === "function" ? validate(value) : [];
      if (!issues?.length) return value;

      ValidationUtilities.addParseWarning(parseWarnings, path, invalidMessage);
      return typeof fallback === "function" ? fallback() : undefined;
    },

    /**
     * Validate a text value.
     * @param {*} value Candidate value.
     * @param {object} [options] Validation options.
     * @param {string} [options.field] Field path.
     * @param {boolean} [options.required] Whether the value is required.
     * @param {boolean} [options.nonEmptyWhenPresent] Whether present values
     * must be non-empty strings.
     * @param {string} [options.requiredMessage] Optional required-field message.
     * @param {string} [options.invalidMessage] Optional invalid-field message.
     * @returns {Array<object>} Validation issues.
     */
    validateText(
      value,
      {
        field = "value",
        required = false,
        nonEmptyWhenPresent = true,
        requiredMessage = `${field} is required and must be a non-empty string.`,
        invalidMessage = `${field} must be a non-empty string when present.`,
      } = {},
    ) {
      if (
        !required &&
        (!nonEmptyWhenPresent || value === null || value === undefined)
      ) {
        return [];
      }
      if (isNonEmptyString(value)) return [];
      return [
        ValidationUtilities.createValidationIssue({
          field,
          message: required ? requiredMessage : invalidMessage,
        }),
      ];
    },

    /**
     * Validate a boolean value.
     * @param {*} value Candidate value.
     * @param {object} [options] Validation options.
     * @param {string} [options.field] Field path.
     * @param {boolean} [options.required] Whether the value is required.
     * @param {string} [options.requiredMessage] Optional required-field message.
     * @param {string} [options.invalidMessage] Optional invalid-field message.
     * @returns {Array<object>} Validation issues.
     */
    validateBoolean(
      value,
      {
        field = "value",
        required = false,
        requiredMessage = `${field} is required and must be a boolean.`,
        invalidMessage = `${field} must be a boolean when present.`,
      } = {},
    ) {
      if (!required && (value === null || value === undefined)) return [];
      if (typeof value === "boolean") return [];
      return [
        ValidationUtilities.createValidationIssue({
          field,
          message: required ? requiredMessage : invalidMessage,
        }),
      ];
    },

    /**
     * Validate a non-negative integer value.
     * @param {*} value Candidate value.
     * @param {object} [options] Validation options.
     * @param {string} [options.field] Field path.
     * @param {boolean} [options.required] Whether the value is required.
     * @param {string} [options.requiredMessage] Optional required-field message.
     * @param {string} [options.invalidMessage] Optional invalid-field message.
     * @returns {Array<object>} Validation issues.
     */
    validateInteger(
      value,
      {
        field = "value",
        required = false,
        requiredMessage = `${field} is required and must be a non-negative integer.`,
        invalidMessage = `${field} must be a non-negative integer when present.`,
      } = {},
    ) {
      if (!required && (value === null || value === undefined)) return [];
      if (isNonNegativeInteger(value)) return [];
      return [
        ValidationUtilities.createValidationIssue({
          field,
          message: required ? requiredMessage : invalidMessage,
        }),
      ];
    },

    /**
     * Validate a date value.
     * @param {*} value Candidate value.
     * @param {object} [options] Validation options.
     * @param {string} [options.field] Field path.
     * @param {boolean} [options.required] Whether the value is required.
     * @param {string} [options.requiredMessage] Optional required-field message.
     * @param {string} [options.invalidMessage] Optional invalid-field message.
     * @returns {Array<object>} Validation issues.
     */
    validateDate(
      value,
      {
        field = "value",
        required = false,
        requiredMessage = `${field} is required and must be a valid date.`,
        invalidMessage = `${field} must be a valid date.`,
      } = {},
    ) {
      if (!required && (value === null || value === undefined)) return [];
      if (DateUtilities.isValidDate(value)) return [];
      return [
        ValidationUtilities.createValidationIssue({
          field,
          message: required ? requiredMessage : invalidMessage,
        }),
      ];
    },

    /**
     * Clone validation error objects.
     * @param {Array<object>} errors Error objects to clone.
     * @returns {Array<object>} Cloned validation errors.
     */
    cloneValidationErrors(errors) {
      if (!Array.isArray(errors)) return [];
      return errors.map((error) => ({ ...error }));
    },

    /**
     * Build a report object from validation issues.
     * @param {Array<object>} issues Validation issues.
     * @returns {{valid:boolean, issues:Array<object>, errors:Array<object>,
     * warnings:Array<object>}} Validation report.
     */
    createValidationReport(issues) {
      const normalizedIssues =
        ValidationUtilities.cloneValidationErrors(issues);
      const errors = normalizedIssues.filter(
        (issue) => (issue?.severity || "error") === "error",
      );
      const warnings = normalizedIssues.filter(
        (issue) => issue?.severity === "warning",
      );

      return {
        valid: errors.length === 0,
        issues: normalizedIssues,
        errors,
        warnings,
      };
    },

    /**
     * Create an Error that carries cloned validation errors.
     * @param {string} message Error message.
     * @param {Array<object>} [validationErrors] Validation errors to attach.
     * @param {object} [extra] Additional fields to assign to the error.
     * @returns {Error} Error with `validationErrors`.
     */
    createValidationException(message, validationErrors = [], extra = {}) {
      const error = new Error(message);
      Object.assign(error, extra);
      error.validationErrors =
        ValidationUtilities.cloneValidationErrors(validationErrors);
      return error;
    },
  };

  return ValidationUtilities;
});