105 lines
3.6 KiB
TypeScript
105 lines
3.6 KiB
TypeScript
|
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<T> extends ErrorWithContext {
|
||
|
constructor(
|
||
|
/** The original input data. */
|
||
|
public readonly data: any,
|
||
|
|
||
|
/** The validator instance used. */
|
||
|
public readonly validator: Validator<T>,
|
||
|
|
||
|
/** 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<T> {
|
||
|
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<T>.
|
||
|
* If it is invalid, a ValidationError is thrown.
|
||
|
* @param data
|
||
|
*/
|
||
|
public async validate(data: unknown): Promise<Valid<T>> {
|
||
|
const messages = await this.validateAndGetErrors(data)
|
||
|
if ( messages.any() ) {
|
||
|
throw new ValidationError<T>(data, this, messages)
|
||
|
}
|
||
|
|
||
|
return data as Valid<T>
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns true if the given data is valid and type aliases it as Valid<T>.
|
||
|
* @param data
|
||
|
*/
|
||
|
public async isValid(data: any): Promise<boolean> {
|
||
|
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: any): Promise<Messages> {
|
||
|
const messages = new Messages()
|
||
|
const params: ValidatorFunctionParams = { data }
|
||
|
|
||
|
for ( const key in this.rules ) {
|
||
|
if ( !this.rules.hasOwnProperty(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<any>(data, key) ) {
|
||
|
let [entry, dataKey] = walkEntry
|
||
|
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)
|
||
|
}
|
||
|
|
||
|
if ( result.stopValidation ) {
|
||
|
break // move on to the next field
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return messages
|
||
|
}
|
||
|
}
|