import {Valid, ValidationResult, ValidationRules, ValidatorFunction, ValidatorFunctionParams} from './rules/types' import {Messages, ErrorWithContext, dataWalkUnsafe, dataSetUnsafe} from '../util' /** * An error thrown thrown when an object fails its validation. */ export class ValidationError extends ErrorWithContext { constructor( /** The original input data. */ public readonly data: unknown, /** The validator instance used. */ public readonly validator: Validator, /** Validation error messages, by field. */ public readonly errors: Messages, ) { super('One or more fields were invalid.', { data, messages: errors.all() }) } } /** * A class to validate arbitrary data using functional rules. */ export class Validator { constructor( /** The rules used to validate input objects. */ protected readonly rules: ValidationRules, ) {} /** * Attempt to validate the input data. * If it is valid, it is type aliased as Valid. * If it is invalid, a ValidationError is thrown. * @param data */ public async validate(data: unknown): Promise> { const messages = await this.validateAndGetErrors(data) if ( messages.any() ) { throw new ValidationError(data, this, messages) } return data as Valid } /** * Returns true if the given data is valid and type aliases it as Valid. * @param data */ public async isValid(data: unknown): Promise { return !(await this.validateAndGetErrors(data)).any() } /** * Apply the validation rules to the data object and return any error messages. * @param data * @protected */ protected async validateAndGetErrors(data: unknown): Promise { const messages = new Messages() const params: ValidatorFunctionParams = { data } for ( const key in this.rules ) { if ( !Object.prototype.hasOwnProperty.call(this.rules, key) ) { continue } // This walks over all of the values in the data structure using the nested // key notation. It's not type-safe, but neither is the original input object // yet, so it's useful here. for ( const walkEntry of dataWalkUnsafe(data as any, key) ) { let [entry, dataKey] = walkEntry // eslint-disable-line prefer-const const rules = (Array.isArray(this.rules[key]) ? this.rules[key] : [this.rules[key]]) as ValidatorFunction[] for ( const rule of rules ) { const result: ValidationResult = await rule(dataKey, entry, params) if ( !result.valid ) { let errors = ['is invalid'] if ( Array.isArray(result.message) && result.message.length ) { errors = result.message } else if ( !Array.isArray(result.message) && result.message ) { errors = [result.message] } for ( const error of errors ) { if ( !messages.has(dataKey, error) ) { messages.put(dataKey, error) } } } if ( result.valid && result.castValue ) { entry = result.castValue data = dataSetUnsafe(dataKey, entry, data as any) } if ( result.stopValidation ) { break // move on to the next field } } } } return messages } }