     1	/**
     2	 * @file Defines the Schema class, which contains the core validation engine.
     3	 */
     4	
     5	/**
     6	 * @typedef {object} ValidationError
     7	 * @property {string} field - The name of the field that failed validation.
     8	 * @property {string} code - A stable, machine-readable error code (e.g., 'MIN_LENGTH').
     9	 * @property {string} message - The human-readable error message.
    10	 * @property {object} [params] - A key-value object with context about the error (e.g., { min: 3, actual: 2 }).
    11	 */
    12	
    13	/**
    14	 * @typedef {object} ValidationContext
    15	 * @property {Schema} schema - The current schema instance.
    16	 * @property {object} definition - The schema definition for the specific field.
    17	 * @property {any} value - The current value of the field being processed.
    18	 * @property {string} fieldName - The name of the field.
    19	 * @property {object} object - The full object being validated (with modifications).
    20	 * @property {object} objectBeforeCast - The original, unmodified input object.
    21	 * @property {any} valueBeforeCast - The original value of the field.
    22	 * @property {object} options - The global validation options.
    23	 * @property {{nullable: boolean, nullOnEmpty: boolean}} computedOptions - Calculated options.
    24	 * @property {string} [parameterName] - The name of the validator parameter being processed.
    25	 * @property {any} [parameterValue] - The value of the validator parameter.
    26	 * @property {function(): void} throwTypeError - Throws a standardized type casting error.
    27	 * @property {function(string, string, object=): void} throwParamError - Throws a standardized parameter validation error.
    28	 */
    29	
    30	/**
    31	 * Represents an instance of a schema that can validate objects against a structure.
    32	 * This class is instantiated by the createSchema factory function.
    33	 */
    34	export class Schema {
    35	  /**
    36	   * @param {object} structure The schema definition.
    37	   * @param {object} types The globally registered type handlers.
    38	   * @param {object} validators The globally registered validator handlers.
    39	   */
    40	  constructor (structure, types, validators) {
    41	    this.structure = structure
    42	    this.types = types // Now passed in directly
    43	    this.validators = validators // Now passed in directly
    44	  }
    45	
    46	  // --- Private Helpers ---
    47	
    48	  /** @private */
    49	  _typeError (field) {
    50	    return this._paramError(field, 'TYPE_CAST_FAILED', 'Value could not be cast to the required type.')
    51	  }
    52	
    53	  /** @private */
    54	  _paramError (field, code, message, params = {}) {
    55	    const e = new Error(message)
    56	    e.errorObject = { field, code, message, params }
    57	    return e
    58	  }
    59	
    60	  /** @private */
    61	  _paramToBeSkipped (parameterName, skipParams, fieldName) {
    62	    if (typeof skipParams !== 'object' || skipParams === null) return false
    63	    if (Array.isArray(skipParams[fieldName]) && skipParams[fieldName].includes(parameterName)) return true
    64	    return false
    65	  }
    66	
    67	  /**
    68	   * Processes a single field through the entire validation pipeline (pre-checks, casting, validators).
    69	   * This is the heart of the validation logic for an individual field.
    70	   * @private
    71	   * @param {string} fieldName - The name of the field to validate.
    72	   * @param {object} object - The original input object.
    73	   * @param {object} validatedObject - The object being built with validated data.
    74	   * @param {object} options - The global validation options.
    75	   * @returns {Promise<ValidationError|null>} An error object if validation fails, otherwise null.
    76	   */
    77	  async _validateField (fieldName, object, validatedObject, options) {
    78	    const definition = this.structure[fieldName]
    79	    if (!definition) return null
    80	
    81	    // --- 1. Pre-validation Checks ---
    82	    if (Array.isArray(options.skipFields) && options.skipFields.includes(fieldName)) return null
    83	
    84	    if (definition.required && object[fieldName] === undefined) {
    85	      if (!this._paramToBeSkipped('required', options.skipParams, fieldName)) {
    86	        return { field: fieldName, code: 'REQUIRED', message: 'Field is required', params: {} }
    87	      }
    88	    }
    89	
    90	    if (object[fieldName] === undefined) {
    91	      // It's not required and it's not present, so we can stop processing this field.
    92	      // The 'defaultTo' value will be applied in the main `validate` loop's post-processing step.
    93	      return null
    94	    }
    95	
    96	    const nullable = definition.nullable === true || options.nullable === true
    97	    const nullOnEmpty = definition.nullOnEmpty === true || options.nullOnEmpty === true
    98	
    99	    if (object[fieldName] === null) {
   100	      return nullable ? null : { field: fieldName, code: 'NOT_NULLABLE', message: 'Field cannot be null', params: {} }
   101	    }
   102	
   103	    if (String(object[fieldName]) === '' && nullOnEmpty) {
   104	      validatedObject[fieldName] = null
   105	      return null
   106	    }
   107	
   108	    /** @type {ValidationContext} */
   109	    const context = {
   110	      schema: this,
   111	      definition,
   112	      value: validatedObject[fieldName],
   113	      fieldName,
   114	      object: validatedObject,
   115	      objectBeforeCast: object,
   116	      valueBeforeCast: object[fieldName],
   117	      options,
   118	      computedOptions: { nullable: nullable || nullOnEmpty, nullOnEmpty },
   119	
   120	      // NEW: Public API for throwing standardized errors from within plugins/handlers
   121	      throwTypeError: () => {
   122	        throw this._typeError(fieldName)
   123	      },
   124	      throwParamError: (code, message, params) => {
   125	        throw this._paramError(fieldName, code, message, params)
   126	      }
   127	    }
   128	
   129	    // --- 2. Type Casting ---
   130	    const typeHandler = this.types[definition.type]
   131	    if (!typeHandler) throw new Error(`No casting function for type: ${definition.type}`)
   132	
   133	    try {
   134	      const castResult = await typeHandler(context)
   135	      if (castResult !== undefined) {
   136	        validatedObject[fieldName] = castResult
   137	        context.value = castResult // Update context for subsequent validators.
   138	      }
   139	    } catch (e) {
   140	      if (e.errorObject) return e.errorObject // It's a validation error.
   141	      throw e // It's an unexpected system error, re-throw it.
   142	    }
   143	
   144	    // --- 3. Parameter Validators ---
   145	    for (const paramName in definition) {
   146	      if (paramName === 'type') continue
   147	      if (this._paramToBeSkipped(paramName, options.skipParams, fieldName)) continue
   148	
   149	      const validatorHandler = this.validators[paramName]
   150	      if (validatorHandler) {
   151	        try {
   152	          context.parameterName = paramName
   153	          context.parameterValue = definition[paramName]
   154	          const validatorResult = await validatorHandler(context)
   155	          if (validatorResult !== undefined) {
   156	            validatedObject[fieldName] = validatorResult
   157	            context.value = validatorResult // Update context value for the next validator.
   158	          }
   159	        } catch (e) {
   160	          if (e.errorObject) return e.errorObject // It's a validation error.
   161	          throw e // It's an unexpected system error.
   162	        }
   163	      }
   164	    }
   165	    return null // Field is valid
   166	  }
   167	
   168	  // --- Public API ---
   169	
   170	  /**
   171	   * Validates an object against the schema structure.
   172	   * This method orchestrates the validation process.
   173	   * @param {object} object - The input object to validate.
   174	   * @param {object} [options={}] - Validation options.
   175	   * @returns {Promise<{validatedObject: object, errors: Object.<string, ValidationError>}>}
   176	   */
   177	  async validate (object, options = {}) {
   178	    const errors = {}
   179	    const validatedObject = { ...object }
   180	    const validationPromises = []
   181	
   182	    // Step 1: Check for fields in the input that are not in the schema.
   183	    for (const fieldName in object) {
   184	      if (this.structure[fieldName] === undefined) {
   185	        errors[fieldName] = { field: fieldName, code: 'FIELD_NOT_ALLOWED', message: 'Field not allowed', params: {} }
   186	      }
   187	    }
   188	
   189	    // Step 2: Determine which fields to iterate over for validation.
   190	    const targetFields = options.onlyObjectValues ? Object.keys(object) : Object.keys(this.structure)
   191	
   192	    // Step 3: Concurrently validate all fields.
   193	    for (const fieldName of targetFields) {
   194	      validationPromises.push(
   195	        this._validateField(fieldName, object, validatedObject, options)
   196	      )
   197	    }
   198	
   199	    // Step 4: Collect results from all validation pipelines.
   200	    const results = await Promise.all(validationPromises)
   201	    for (const error of results) {
   202	      if (error) {
   203	        errors[error.field] = error
   204	      }
   205	    }
   206	
   207	    // Step 5: Post-process for defaultTo on fields that were not present in the input.
   208	    // For full validation (!onlyObjectValues), ensure all schema fields are present.
   209	    if (!options.onlyObjectValues) {
   210	      for (const fieldName in this.structure) {
   211	        // Only process fields that were not in the original input
   212	        if (!(fieldName in object) && validatedObject[fieldName] === undefined) {
   213	          if (this.structure[fieldName].defaultTo !== undefined) {
   214	            const def = this.structure[fieldName].defaultTo
   215	            validatedObject[fieldName] = typeof def === 'function' ? def() : def
   216	          } else {
   217	            // Set fields not in input to null for complete PUT-like records
   218	            validatedObject[fieldName] = null
   219	          }
   220	        }
   221	      }
   222	    }
   223	
   224	    return { validatedObject, errors }
   225	  }
   226	
   227	  /**
   228	   * Utility to filter an object, keeping only fields that have a specific parameter.
   229	   * @param {object} object - The object to clean.
   230	   * @param {string} parameterName - The schema parameter to look for.
   231	   * @returns {object} A new object with only the matching fields.
   232	   */
   233	  cleanup (object, parameterName) {
   234	    const newObject = {}
   235	    for (const k in object) {
   236	      if (this.structure[k] && this.structure[k][parameterName]) {
   237	        newObject[k] = object[k]
   238	      }
   239	    }
   240	    return newObject
   241	  }
   242	}
