import * as plugins from '../plugins.js'; import { ValidationError } from '../errors/base.errors.js'; import type { IBaseConfig } from './base.config.js'; /** * Validation result */ export interface IValidationResult { /** * Whether the validation passed */ valid: boolean; /** * Validation errors if any */ errors?: string[]; /** * Validated configuration (may include defaults) */ config?: any; } /** * Validation schema types */ export type ValidationSchema = Record boolean | string; }>; /** * Configuration validator * Validates configuration objects against schemas and provides default values */ export class ConfigValidator { /** * Basic schema for IBaseConfig */ private static baseConfigSchema: ValidationSchema = { id: { type: 'string', required: false }, version: { type: 'string', required: false }, environment: { type: 'string', required: false, enum: ['development', 'test', 'staging', 'production'], default: 'production' }, name: { type: 'string', required: false }, enabled: { type: 'boolean', required: false, default: true }, logging: { type: 'object', required: false, schema: { level: { type: 'string', required: false, enum: ['error', 'warn', 'info', 'debug'], default: 'info' }, structured: { type: 'boolean', required: false, default: true }, correlationTracking: { type: 'boolean', required: false, default: true } } } }; /** * Validate a configuration object against a schema * * @param config Configuration object to validate * @param schema Validation schema * @returns Validation result */ public static validate(config: T, schema: ValidationSchema): IValidationResult { const errors: string[] = []; const validatedConfig = { ...config }; // Validate each field against the schema for (const [key, rules] of Object.entries(schema)) { const value = config[key]; // Check if required if (rules.required && (value === undefined || value === null)) { errors.push(`${key} is required`); continue; } // If not present and not required, apply default if available if ((value === undefined || value === null)) { if (rules.default !== undefined) { validatedConfig[key] = rules.default; } continue; } // Type validation if (value !== undefined && value !== null) { const valueType = Array.isArray(value) ? 'array' : typeof value; if (valueType !== rules.type) { errors.push(`${key} must be of type ${rules.type}, got ${valueType}`); continue; } // Type-specific validations switch (rules.type) { case 'number': if (rules.min !== undefined && value < rules.min) { errors.push(`${key} must be at least ${rules.min}`); } if (rules.max !== undefined && value > rules.max) { errors.push(`${key} must be at most ${rules.max}`); } break; case 'string': if (rules.minLength !== undefined && value.length < rules.minLength) { errors.push(`${key} must be at least ${rules.minLength} characters`); } if (rules.maxLength !== undefined && value.length > rules.maxLength) { errors.push(`${key} must be at most ${rules.maxLength} characters`); } if (rules.pattern && !rules.pattern.test(value)) { errors.push(`${key} must match pattern ${rules.pattern}`); } break; case 'array': if (rules.minLength !== undefined && value.length < rules.minLength) { errors.push(`${key} must have at least ${rules.minLength} items`); } if (rules.maxLength !== undefined && value.length > rules.maxLength) { errors.push(`${key} must have at most ${rules.maxLength} items`); } if (rules.items && value.length > 0) { for (let i = 0; i < value.length; i++) { const itemType = Array.isArray(value[i]) ? 'array' : typeof value[i]; if (itemType !== rules.items.type) { errors.push(`${key}[${i}] must be of type ${rules.items.type}, got ${itemType}`); } else if (rules.items.schema && itemType === 'object') { const itemResult = this.validate(value[i], rules.items.schema); if (!itemResult.valid) { errors.push(...itemResult.errors.map(err => `${key}[${i}].${err}`)); } } } } break; case 'object': if (rules.schema) { const nestedResult = this.validate(value, rules.schema); if (!nestedResult.valid) { errors.push(...nestedResult.errors.map(err => `${key}.${err}`)); } validatedConfig[key] = nestedResult.config; } break; } // Enum validation if (rules.enum && !rules.enum.includes(value)) { errors.push(`${key} must be one of [${rules.enum.join(', ')}]`); } // Custom validation if (rules.validate) { const result = rules.validate(value); if (result !== true) { errors.push(typeof result === 'string' ? result : `${key} failed custom validation`); } } } } return { valid: errors.length === 0, errors: errors.length > 0 ? errors : undefined, config: validatedConfig }; } /** * Validate base configuration * * @param config Base configuration * @returns Validation result for base configuration */ public static validateBaseConfig(config: IBaseConfig): IValidationResult { return this.validate(config, this.baseConfigSchema); } /** * Apply defaults to a configuration object based on a schema * * @param config Configuration object to apply defaults to * @param schema Validation schema with defaults * @returns Configuration with defaults applied */ public static applyDefaults(config: T, schema: ValidationSchema): T { const result = { ...config }; for (const [key, rules] of Object.entries(schema)) { if (result[key] === undefined && rules.default !== undefined) { result[key] = rules.default; } // Apply defaults to nested objects if (result[key] && rules.type === 'object' && rules.schema) { result[key] = this.applyDefaults(result[key], rules.schema); } // Apply defaults to array items if (result[key] && rules.type === 'array' && rules.items && rules.items.schema) { result[key] = result[key].map(item => typeof item === 'object' ? this.applyDefaults(item, rules.items.schema) : item ); } } return result; } /** * Throw a validation error if the configuration is invalid * * @param config Configuration to validate * @param schema Validation schema * @returns Validated configuration with defaults * @throws ValidationError if validation fails */ public static validateOrThrow(config: T, schema: ValidationSchema): T { const result = this.validate(config, schema); if (!result.valid) { throw new ValidationError( `Configuration validation failed: ${result.errors.join(', ')}`, 'CONFIG_VALIDATION_ERROR', { data: { errors: result.errors } } ); } return result.config; } }