diff --git a/packages/core/src/V.spec.ts b/packages/core/src/V.spec.ts index 5d7dc07..a653253 100644 --- a/packages/core/src/V.spec.ts +++ b/packages/core/src/V.spec.ts @@ -69,7 +69,8 @@ class DeferredValidator extends Validator { describe('ValidationResult', () => { test('getValue() returns valid value', async () => { const result = await V.string().validate('123'); - expect(result.getValue()).toEqual('123'); + const str: string = result.getValue(); + expect(str).toEqual('123'); }); test('getValue() throws ValidationError if validation failed', async () => { @@ -236,8 +237,6 @@ describe('boolean', () => { }); }); -test('empty next', () => expectValid(1, V.number().next())); - describe('uuid', () => { test('null is not valid', () => expectViolations(null, V.uuid(), defaultViolations.notNull())); @@ -400,12 +399,12 @@ describe('objects', () => { } } - const modelValidator = V.object({ + const modelValidator: Validator = V.object({ properties: { password1: V.allOf(V.string(), V.notEmpty()), - password2: [V.string(), V.notEmpty()], + password2: V.compositionOf(V.string(), V.notEmpty()), }, - }).nextMap(value => new PasswordRequest(value as IPasswordRequest)); + }).nextMap(value => new PasswordRequest(value)); const validator = modelValidator.next( V.assertTrue((request: PasswordRequest) => request.password1 === request.password2, 'ConfirmPassword', property('password1')), @@ -419,7 +418,11 @@ describe('objects', () => { }); describe('recursive models', () => { - const validator = V.object({ + interface RecursiveModel { + first: string; + next?: RecursiveModel; + } + const validator: Validator = V.object({ properties: { first: V.string(), next: V.optional(V.fn((value: any, path: Path, ctx: ValidationContext) => validator.validatePath(value, path, ctx))), @@ -437,11 +440,11 @@ describe('objects', () => { }); describe('custom property filtering ObjectValidator extension', () => { - class DropAllPropertiesValidator extends ObjectValidator { + class DropAllPropertiesValidator extends ObjectValidator> { constructor(model: ObjectModel) { super(model); } - validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike { + validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike>> { return this.validateFilteredPath(value, path, ctx, _ => false); } } @@ -663,7 +666,12 @@ describe('object next', () => { }); describe('object localNext', () => { - const parent = V.object({ + interface Parent { + name: string; + upper?: boolean; + } + interface Child extends Parent {} + const parent = V.object({ properties: { name: V.string(), upper: V.optional(V.boolean()), @@ -676,7 +684,7 @@ describe('object localNext', () => { }), localNext: V.map(obj => `parent:${obj.name}`), }); - const child = V.object({ + const child = V.object({ extends: parent, localNext: V.map(obj => `child:${obj.name}`), }); @@ -890,11 +898,11 @@ describe('arrays', () => { test('single value to array of one', () => expectValid('foo', stringArray, ['foo'])); - test('any item is allowed', () => expectValid(['string'], V.toArray())); + test('any item is allowed', () => expectValid(['string'], V.toArray(V.string()))); }); describe('size', () => { - test('valid', () => expectValid([1], V.size(1, 1))); + test('valid', () => expectValid([1], V.size(1, 1))); test('too short', () => expectViolations([1], V.size(2, 3), defaultViolations.size(2, 3))); @@ -1131,11 +1139,11 @@ describe('groups', () => { }); test('whenGroup not allowed after otherwise', () => { - expect(() => (V.whenGroup('any').otherwise() as WhenGroupValidator).whenGroup('foo')).toThrow(); + expect(() => (V.whenGroup('any', V.any()).otherwise(V.any()) as WhenGroupValidator).whenGroup('foo', V.any())).toThrow(); }); test('otherwise not allowed after otherwise', () => { - expect(() => (V.whenGroup('any').otherwise() as WhenGroupValidator).otherwise()).toThrow(); + expect(() => (V.whenGroup('any', V.any()).otherwise(V.any()) as WhenGroupValidator).otherwise(V.any())).toThrow(); }); test('Group.of', () => { @@ -1226,11 +1234,11 @@ describe('if', () => { test('else matches invalid', () => expectViolations({}, validator, defaultViolations.string({}))); test('defining else before elseif is not allowed', () => { - expect(() => (V.if(_ => true).else() as IfValidator).elseIf(_ => true)).toThrow(); + expect(() => (V.if(_ => true, V.any()).else(V.any()) as IfValidator).elseIf(_ => true, V.any())).toThrow(); }); test('redefining else is not allowed', () => { - expect(() => (V.if(_ => true).else() as IfValidator).else()).toThrow(); + expect(() => (V.if(_ => true, V.any()).else(V.any()) as IfValidator).else(V.any())).toThrow(); }); test('no-conditional anomaly', () => expectValid({}, new IfValidator([]))); @@ -1270,6 +1278,8 @@ describe('normalizers', () => { test('empty string', () => expectValid('', V.emptyTo('default'), 'default')); + test('empty array', () => expectValid([], V.emptyTo(['default']), ['default'])); + test('anything else is passed as is', () => expectValid('anything', V.emptyTo('default'))); }); diff --git a/packages/core/src/V.ts b/packages/core/src/V.ts index cd93625..dcf88df 100644 --- a/packages/core/src/V.ts +++ b/packages/core/src/V.ts @@ -23,7 +23,6 @@ import { MappingFn, Validator, CheckValidator, - maybeAllOfValidator, OptionalValidator, AssertTrue, IfValidator, @@ -60,7 +59,6 @@ const ignoreValidator = new IgnoreValidator(), anyValidator = new AnyValidator(), stringValidator = new StringValidator(), toStringValidator = new StringNormalizer(), - notNullValidator = new NotNullOrUndefinedValidator(), nullOrUndefinedValidator = new IsNullOrUndefinedValidator(), notEmptyValidator = new NotEmptyValidator(), notBlankValidator = new NotBlankValidator(), @@ -75,29 +73,47 @@ const ignoreValidator = new IgnoreValidator(), dateValidator = new DateValidator(ValidatorType.Date); export const V = { - fn: (fn: ValidatorFn, type?: string) => new ValidatorFnWrapper(fn, type), + fn: (fn: ValidatorFn, type?: string) => new ValidatorFnWrapper(fn, type), - map: (fn: MappingFn, error?: any) => new ValueMapper(fn, error), + map: (fn: MappingFn, error?: any) => new ValueMapper(fn, error), ignore: () => ignoreValidator, any: () => anyValidator, - check: (...allOf: Validator[]) => new CheckValidator(maybeAllOfValidator(allOf)), - - optional: (type: Validator, ...allOf: Validator[]) => new OptionalValidator(type, allOf), - - required: (type: Validator, ...allOf: Validator[]) => new RequiredValidator(type, allOf), - - if: (fn: AssertTrue, ...allOf: Validator[]) => new IfValidator([new Conditional(fn, allOf)]), - - whenGroup: (group: GroupOrName, ...allOf: Validator[]) => new WhenGroupValidator([new WhenGroup(group, allOf)]), + check: (...validators: [...Validator[], Validator]) => { + if (validators.length === 1) { + return new CheckValidator(validators[0] as Validator); + } else { + return new CheckValidator(new CompositionValidator(validators)) + } + }, + + optional: (...validators: [...Validator[], Validator]) => { + if (validators.length === 1) { + return new OptionalValidator(validators[0] as Validator); + } else { + return new OptionalValidator(new CompositionValidator(validators)) + } + }, + + required: (...validators: [...Validator[], Validator]) => { + if (validators.length === 1) { + return new RequiredValidator(validators[0] as Validator); + } else { + return new RequiredValidator(new CompositionValidator(validators)) + } + }, + + if: (fn: AssertTrue, validator: Validator) => new IfValidator([new Conditional(fn, validator)]), + + whenGroup: (group: GroupOrName, validator: Validator) => new WhenGroupValidator([new WhenGroup(group, validator)]), string: () => stringValidator, toString: () => toStringValidator, - notNull: () => notNullValidator, + notNull: () => new NotNullOrUndefinedValidator(), nullOrUndefined: () => nullOrUndefinedValidator, @@ -111,7 +127,7 @@ export const V = { undefinedToNull: () => undefinedToNullValidator, - emptyTo: (defaultValue: any) => new ValueMapper((value: any) => (isNullOrUndefined(value) || value === '' ? defaultValue : value)), + emptyTo: (defaultValue: T) => new ValueMapper((value: T) => (isNullOrUndefined(value) || value.length === 0 ? defaultValue : value)), uuid: (version?: number) => new UuidValidator(version), @@ -135,11 +151,11 @@ export const V = { max: (max: number, inclusive = true) => new MaxValidator(max, inclusive), - object: (model: ObjectModel) => new ObjectValidator(model), + object: (model: ObjectModel) => new ObjectValidator(model), toObject: (property: string) => new ObjectNormalizer(property), - schema: (fn: (schema: SchemaValidator) => SchemaModel) => new SchemaValidator(fn), + schema: (fn: (schema: SchemaValidator) => SchemaModel) => new SchemaValidator(fn), /** WARN: Objects as Map keys use identity hash/equals, i.e. === */ mapType: (keys: Validator, values: Validator, jsonSafeMap: boolean = true) => new MapValidator(keys, values, jsonSafeMap), @@ -155,30 +171,30 @@ export const V = { nullToArray: () => new ValueMapper((value: any) => (isNullOrUndefined(value) ? [] : value)), - array: (...items: Validator[]) => new ArrayValidator(maybeAllOfValidator(items)), + array: (items: Validator) => new ArrayValidator(items), - toArray: (...items: Validator[]) => new ArrayNormalizer(maybeAllOfValidator(items)), + toArray: (items: Validator) => new ArrayNormalizer(items), - size: (min: number, max: number) => new SizeValidator(min, max), + size: (min: number, max: number) => new SizeValidator(min, max), - properties: (keys: Validator | Validator[], values: Validator | Validator[]) => new ObjectValidator({ additionalProperties: { keys, values } }), + properties: (keys: Validator, values: Validator) => new ObjectValidator({ additionalProperties: { keys, values } }), - allOf: (...validators: Validator[]) => new AllOfValidator(validators), + allOf: (...validators: [Validator, ...Validator[]]) => new AllOfValidator(validators), - anyOf: (...validators: Validator[]) => new AnyOfValidator(validators), + anyOf: (...validators: [Validator, ...Validator[]]) => new AnyOfValidator(validators), - oneOf: (...validators: Validator[]) => new OneOfValidator(validators), + oneOf: (...validators: [Validator, ...Validator[]]) => new OneOfValidator(validators), - compositionOf: (...validators: Validator[]) => new CompositionValidator(validators), + compositionOf: (...validators: [...Validator[], Validator]) => new CompositionValidator(validators), date: () => dateValidator, - enum: (enumType: object, name: string) => new EnumValidator(enumType, name), + enum: (enumType: T, name: string) => new EnumValidator(enumType, name), - assertTrue: (fn: AssertTrue, type: string = 'AssertTrue', path?: Path) => new AssertTrueValidator(fn, type, path), + assertTrue: (fn: AssertTrue, type: string = 'AssertTrue', path?: Path) => new AssertTrueValidator(fn, type, path), - hasValue: (expectedValue: any) => new HasValueValidator(expectedValue), + hasValue: (expectedValue: T) => new HasValueValidator(expectedValue), - json: (...validators: Validator[]) => new JsonValidator(validators), + json: (validator: Validator) => new JsonValidator(validator), }; Object.freeze(V); diff --git a/packages/core/src/schema.ts b/packages/core/src/schema.ts index 6dd0186..64c6324 100644 --- a/packages/core/src/schema.ts +++ b/packages/core/src/schema.ts @@ -27,21 +27,21 @@ export interface SchemaModel { export type ClassParentModel = string | ObjectValidator | (string | ObjectValidator)[]; -export interface ClassModel { +export interface ClassModel { readonly properties?: PropertyModel; readonly additionalProperties?: boolean | MapEntryModel | MapEntryModel[]; readonly extends?: ClassParentModel; readonly localProperties?: PropertyModel; - readonly next?: Validator; + readonly next?: Validator; readonly localNext?: Validator; } -export class ModelRef extends Validator { - constructor(private schema: SchemaValidator, public readonly name: string) { +export class ModelRef extends Validator { + constructor(private schema: SchemaValidator, public readonly name: string) { super(); Object.freeze(this); } - validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike { + validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike> { return this.schema.validateClass(value, path, ctx, this.name); } } @@ -52,14 +52,14 @@ export class DiscriminatorViolation extends Violation { } } -export class SchemaValidator extends Validator { +export class SchemaValidator extends Validator { public readonly discriminator: Discriminator; private readonly proxies = new Map(); private readonly validators: { [name: string]: Validator } = {}; - constructor(fn: (schema: SchemaValidator) => SchemaModel) { + constructor(fn: (schema: SchemaValidator) => SchemaModel) { super(); const schema = fn(this); for (const name of this.proxies.keys()) { @@ -74,11 +74,11 @@ export class SchemaValidator extends Validator { Object.freeze(this); } - validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike { + validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike> { return this.validateClass(value, path, ctx); } - validateClass(value: any, path: Path, ctx: ValidationContext, expectedType?: string): PromiseLike { + validateClass(value: any, path: Path, ctx: ValidationContext, expectedType?: string): PromiseLike> { if (isNullOrUndefined(value)) { return ctx.failurePromise(defaultViolations.notNull(path), value); } @@ -105,7 +105,7 @@ export class SchemaValidator extends Validator { } // 3) Validate value - return validator.validatePath(value, path, ctx); + return validator.validatePath(value, path, ctx) as PromiseLike>; } of(name: string): Validator { diff --git a/packages/core/src/validators.ts b/packages/core/src/validators.ts index a5e1aef..65b4a1f 100644 --- a/packages/core/src/validators.ts +++ b/packages/core/src/validators.ts @@ -4,12 +4,12 @@ import { validate as uuidValidate, version as uuidVersion } from 'uuid'; const ROOT = Path.ROOT; -export interface ValidatorFn { - (value: any, path: Path, ctx: ValidationContext): PromiseLike; +export interface ValidatorFn{ + (value: any, path: Path, ctx: ValidationContext): PromiseLike>; } -export interface MappingFn { - (value: any, path: Path, ctx: ValidationContext): any | Promise; +export interface MappingFn { + (value: I, path: Path, ctx: ValidationContext): T | PromiseLike; } export type Properties = { [s: string]: Validator }; @@ -39,19 +39,19 @@ export class ValidationContext { } return new ValidationResult(violations); } - success(value: any) { - return new ValidationResult(undefined, value); + success(value: T) { + return new ValidationResult(undefined, value); } failurePromise(violation: Violation | Violation[], value: any) { return this.promise(this.failure(violation, value)); } - successPromise(value: any) { + successPromise(value: T) { return this.promise(this.success(value)); } - promise(result: ValidationResult) { + promise(result: ValidationResult) { return new SyncPromise(result); } - registerObject(value: any, path: Path, convertedValue: any): undefined | ValidationResult { + registerObject(value: any, path: Path, convertedValue: T): undefined | ValidationResult { if (this.objects.has(value)) { if (this.options.allowCycles) { return this.success(this.objects.get(value)); @@ -95,26 +95,23 @@ export class SyncPromise implements PromiseLike { } } -export abstract class Validator { - validateGroup(value: any, group: Group): Promise { +export abstract class Validator { + validateGroup(value: any, group: Group): Promise> { return this.validate(value, { group }); } - validate(value: any, options?: ValidatorOptions): Promise { + validate(value: any, options?: ValidatorOptions): Promise> { return Promise.resolve(this.validatePath(value, ROOT, new ValidationContext(options || {}))); } - abstract validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike; + abstract validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike>; - next(...allOf: Validator[]): Validator { - if (allOf.length == 0) { - return this; - } - return new NextValidator(this, maybeAllOfValidator(allOf)); + next(validator: Validator): Validator { + return new NextValidator(this, validator); } - nextMap(fn: MappingFn): Validator { - return this.next(new ValueMapper(fn)); + nextMap(fn: MappingFn): Validator { + return this.next(new ValueMapper(fn)); } } @@ -122,14 +119,11 @@ export interface WarnLogger { (violation: Violation, ctx: ValidatorOptions): void; } -export class ValidationResult { - private value?: unknown; - - private violations?: Violation[]; - - constructor(violations?: Violation[], value?: unknown) { - this.violations = violations; - this.value = value; +export class ValidationResult { + constructor(private readonly violations?: Violation[], private readonly value?: T) { + if (violations?.length && value !== undefined) { + throw new Error('both violations and success value defined'); + } Object.freeze(this.violations); } @@ -141,11 +135,11 @@ export class ValidationResult { return !this.isSuccess(); } - getValue(): unknown { + getValue(): T { if (!this.isSuccess()) { throw new ValidationError(this.getViolations()); } - return this.value; + return this.value!; } getViolations(): Violation[] { @@ -317,26 +311,47 @@ export const defaultViolations = { cycle: (path: Path) => new Violation(path, 'Cycle'), }; -export interface AssertTrue { - (value: any, path: Path, ctx: ValidationContext): boolean; +export interface AssertTrue { + (value: T, path: Path, ctx: ValidationContext): boolean; } -export type PropertyModel = { [s: string]: string | number | Validator | Validator[] }; +export type PropertyModel = { [s: string]: string | number | Validator }; export type ParentModel = ObjectModel | ObjectValidator | (ObjectModel | ObjectValidator)[]; export interface ObjectModel { + /** + * Inherit all non-local rules from parent validators. + */ readonly extends?: ParentModel; + /** + * Inheritable property rules. + */ readonly properties?: PropertyModel; + /** + * Local, non-inheritable property rules, e.g. discriminator property in a class hierarchy. + */ readonly localProperties?: PropertyModel; + /** + * Validation rules for additional properties. True allows any additional property. + * With MapEntryModel valueValidator must match if keyValidator matches and at least one keyValidator must match. + */ readonly additionalProperties?: boolean | MapEntryModel | MapEntryModel[]; - readonly next?: Validator | Validator[]; + /** + * Next validator to be executed after all properties are validated successfully. + * Use this to define additional rules or conversions for the ObjectValidator. + * Using the `next` function returns a `NextValidator` that cannot be further extended. + */ + readonly next?: Validator; + /** + * Local, non-inheritable rules. + */ readonly localNext?: Validator; } -export interface MapEntryModel { - readonly keys: Validator | Validator[]; - readonly values: Validator | Validator[]; +export interface MapEntryModel { + readonly keys: Validator; + readonly values: Validator; } function getPropertyValidators(properties?: PropertyModel): Properties { @@ -346,7 +361,7 @@ function getPropertyValidators(properties?: PropertyModel): Properties { if (isString(properties[name]) || isNumber(properties[name])) { propertyValidators[name] = new HasValueValidator(properties[name]); } else { - propertyValidators[name] = maybeAllOfValidator(properties[name] as Validator | Validator[]); + propertyValidators[name] = properties[name] as Validator; } } } @@ -388,16 +403,13 @@ function getMapEntryValidators(additionalProperties?: boolean | MapEntryModel | return validators; } -export class ValidatorFnWrapper extends Validator { - private readonly fn: ValidatorFn; - - constructor(fn: ValidatorFn, public readonly type?: string) { +export class ValidatorFnWrapper extends Validator { + constructor(private readonly fn: ValidatorFn, public readonly type?: string) { super(); - this.fn = fn; Object.freeze(this); } - validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike { + validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike> { return this.fn(value, path, ctx); } } @@ -427,7 +439,7 @@ export function mergeProperties(from: Properties, to: Properties): Properties { return to; } -export class ObjectValidator extends Validator { +export class ObjectValidator extends Validator { public readonly properties: Properties; public readonly localProperties: Properties; @@ -436,15 +448,15 @@ export class ObjectValidator extends Validator { public readonly parentValidators: ObjectValidator[]; - public readonly nextValidator: undefined | Validator; + public readonly nextValidator?: Validator; - public readonly localNextValidator: undefined | Validator; + public readonly localNextValidator?: Validator; constructor(public readonly model: ObjectModel) { super(); let properties: Properties = {}; let additionalProperties: MapEntryValidator[] = []; - let inheritedThenValidators: Validator[] = []; + let nextValidators: Validator[] = []; this.parentValidators = getParentValidators(model.extends); for (let i = 0; i < this.parentValidators.length; i++) { @@ -452,21 +464,16 @@ export class ObjectValidator extends Validator { additionalProperties = additionalProperties.concat(parent.additionalProperties); properties = mergeProperties(parent.properties, properties); if (parent.nextValidator) { - inheritedThenValidators = inheritedThenValidators.concat(parent.nextValidator); + nextValidators.push(parent.nextValidator); } } - let nextValidator = inheritedThenValidators.length ? maybeAllOfValidator(inheritedThenValidators) : undefined; if (model.next) { - if (nextValidator) { - nextValidator = nextValidator.next(maybeAllOfValidator(model.next)); - } else { - nextValidator = maybeAllOfValidator(model.next); - } + nextValidators.push(model.next); } this.additionalProperties = additionalProperties.concat(getMapEntryValidators(model.additionalProperties)); this.properties = mergeProperties(getPropertyValidators(model.properties), properties); this.localProperties = getPropertyValidators(model.localProperties); - this.nextValidator = nextValidator; + this.nextValidator = nextValidators.length ? nextValidators.length === 1 ? nextValidators[0] : new CompositionValidator(nextValidators) : undefined; this.localNextValidator = model.localNext; Object.freeze(this.properties); @@ -476,11 +483,11 @@ export class ObjectValidator extends Validator { Object.freeze(this); } - validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike { + validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike> { return this.validateFilteredPath(value, path, ctx, _ => true); } - validateFilteredPath(value: any, path: Path, ctx: ValidationContext, filter: PropertyFilter): PromiseLike { + validateFilteredPath(value: any, path: Path, ctx: ValidationContext, filter: PropertyFilter): PromiseLike> { if (isNullOrUndefined(value)) { return ctx.failurePromise(defaultViolations.notNull(path), value); } @@ -496,7 +503,7 @@ export class ObjectValidator extends Validator { }; const cycleResult = ctx.registerObject(value, path, context.convertedObject); if (cycleResult) { - return ctx.promise(cycleResult); + return ctx.promise(cycleResult as ValidationResult); } const propertyResults: PromiseLike[] = []; @@ -621,23 +628,22 @@ export class ObjectNormalizer extends Validator { export class MapEntryValidator { public readonly keyValidator: Validator; - public readonly valueValidator: Validator; constructor(entryModel: MapEntryModel) { - this.keyValidator = maybeAllOfValidator(entryModel.keys); - this.valueValidator = maybeAllOfValidator(entryModel.values); + this.keyValidator = entryModel.keys; + this.valueValidator = entryModel.values; Object.freeze(this); } } -export class ArrayValidator extends Validator { - constructor(public readonly itemsValidator: Validator) { +export class ArrayValidator extends Validator { + constructor(public readonly itemsValidator: Validator) { super(); Object.freeze(this); } - validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike { + validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike> { if (isNullOrUndefined(value)) { return ctx.failurePromise(defaultViolations.notNull(path), value); } @@ -673,11 +679,11 @@ export class ArrayValidator extends Validator { } } -export class ArrayNormalizer extends ArrayValidator { - constructor(itemsValidator: Validator) { +export class ArrayNormalizer extends ArrayValidator { + constructor(itemsValidator: Validator) { super(itemsValidator); } - validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike { + validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike> { if (value === undefined) { return super.validatePath([], path, ctx); } @@ -688,29 +694,30 @@ export class ArrayNormalizer extends ArrayValidator { } } -export class NextValidator extends Validator { - constructor(public readonly firstValidator: Validator, public readonly nextValidator: Validator) { +export class NextValidator extends Validator { + constructor(public readonly firstValidator: Validator, public readonly nextValidator: Validator) { super(); Object.freeze(this); } - validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike { + validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike> { return this.firstValidator.validatePath(value, path, ctx).then(firstResult => { if (firstResult.isSuccess()) { return this.nextValidator.validatePath(firstResult.getValue(), path, ctx); } - return firstResult; + // Violations without value is essentially same for both + return firstResult as unknown as ValidationResult; }); } } -export class CheckValidator extends Validator { - constructor(public readonly validator: Validator) { +export class CheckValidator extends Validator { + constructor(public readonly validator: Validator) { super(); Object.freeze(this); } - validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike { + validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike> { return this.validator.validatePath(value, path, ctx).then(result => { if (result.isSuccess()) { return ctx.successPromise(value); @@ -720,38 +727,35 @@ export class CheckValidator extends Validator { } } -export class CompositionValidator extends Validator { - public readonly validators: Validator[]; - - constructor(validators: Validator | Validator[]) { +export class CompositionValidator extends Validator { + constructor(public readonly validators: Validator[]) { super(); - this.validators = ([] as Validator[]).concat(validators); Object.freeze(this.validators); Object.freeze(this); } - async validatePath(value: any, path: Path, ctx: ValidationContext): Promise { + async validatePath(value: any, path: Path, ctx: ValidationContext): Promise> { let currentValue = value; for (let i = 0; i < this.validators.length; i++) { const result = await this.validators[i].validatePath(currentValue, path, ctx); if (result.isSuccess()) { currentValue = result.getValue(); } else { - return result; + return result as ValidationResult; } } return ctx.success(currentValue); } } -export class OneOfValidator extends Validator { - constructor(public readonly validators: Validator[]) { +export class OneOfValidator extends Validator { + constructor(public readonly validators: [Validator, ...Validator[]]) { super(); Object.freeze(this.validators); Object.freeze(this); } - async validatePath(value: any, path: Path, ctx: ValidationContext): Promise { + async validatePath(value: any, path: Path, ctx: ValidationContext): Promise> { let matches = 0; let newValue: any = null; const promises: PromiseLike[] = []; @@ -773,7 +777,7 @@ export class OneOfValidator extends Validator { } export class AnyOfValidator extends Validator { - constructor(public readonly validators: Validator[]) { + constructor(public readonly validators: [Validator, ...Validator[]]) { super(); Object.freeze(this.validators); Object.freeze(this); @@ -792,14 +796,14 @@ export class AnyOfValidator extends Validator { } } -export class IfValidator extends Validator { - constructor(public readonly conditionals: Conditional[], public readonly elseValidator?: Validator) { +export class IfValidator extends Validator { + constructor(public readonly conditionals: Conditional[], public readonly elseValidator?: Validator) { super(); Object.freeze(this.conditionals); Object.freeze(this); } - validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike { + validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike> { for (let i = 0; i < this.conditionals.length; i++) { const conditional = this.conditionals[i]; if (conditional.fn(value, path, ctx)) { @@ -812,41 +816,36 @@ export class IfValidator extends Validator { return ctx.successPromise(value); } - elseIf(fn: AssertTrue, ...allOf: Validator[]): IfValidator { + elseIf(fn: AssertTrue, validator: Validator): IfValidator { if (this.elseValidator) { throw new Error('Else is already defined. Define elseIfs first.'); } - return new IfValidator([...this.conditionals, new Conditional(fn, allOf)], this.elseValidator); + return new IfValidator([...this.conditionals, new Conditional(fn, validator)], this.elseValidator); } - else(...allOf: Validator[]): Validator { + else(validator: Validator): IfValidator { if (this.elseValidator) { throw new Error('Else is already defined.'); } - return new IfValidator(this.conditionals, maybeAllOfValidator(allOf)); + return new IfValidator(this.conditionals, validator); } } -export class Conditional { - public readonly fn: AssertTrue; - public readonly validator: Validator; - - constructor(fn: AssertTrue, allOf: Validator[]) { - this.fn = fn; - this.validator = maybeAllOfValidator(allOf); +export class Conditional { + constructor(public readonly fn: AssertTrue, public readonly validator: Validator) { Object.freeze(this.validator); Object.freeze(this); } } -export class WhenGroupValidator extends Validator { - constructor(public readonly whenGroups: WhenGroup[], public readonly otherwiseValidator?: Validator) { +export class WhenGroupValidator extends Validator { + constructor(public readonly whenGroups: WhenGroup[], public readonly otherwiseValidator?: Validator) { super(); Object.freeze(this.whenGroups); Object.freeze(this); } - validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike { + validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike>{ if (ctx.options.group) { for (let i = 0; i < this.whenGroups.length; i++) { const whenGroup = this.whenGroups[i]; @@ -861,38 +860,35 @@ export class WhenGroupValidator extends Validator { return ctx.successPromise(value); } - whenGroup(group: GroupOrName, ...allOf: Validator[]) { + whenGroup(group: GroupOrName, validator: Validator): WhenGroupValidator { if (this.otherwiseValidator) { throw new Error('Otherwise already defined. Define whenGroups first.'); } - return new WhenGroupValidator([...this.whenGroups, new WhenGroup(group, allOf)], this.otherwiseValidator); + return new WhenGroupValidator([...this.whenGroups, new WhenGroup(group, validator)], this.otherwiseValidator); } - otherwise(...allOf: Validator[]): Validator { + otherwise(validator: Validator): Validator { if (this.otherwiseValidator) { throw new Error('Otherwise already defined.'); } - return new WhenGroupValidator(this.whenGroups, maybeAllOfValidator(allOf)); + return new WhenGroupValidator(this.whenGroups, validator); } } -export class WhenGroup { +export class WhenGroup { public readonly group: string; - public readonly validator: Validator; - - constructor(group: GroupOrName, allOf: Validator[]) { + constructor(group: GroupOrName, public readonly validator: Validator) { this.group = isString(group) ? (group as string) : (group as Group).name; - this.validator = maybeAllOfValidator(allOf); Object.freeze(this); } } -export class MapValidator extends Validator { - constructor(public readonly keys: Validator, public readonly values: Validator, public readonly jsonSafeMap: boolean) { +export class MapValidator extends Validator> { + constructor(public readonly keys: Validator, public readonly values: Validator, public readonly jsonSafeMap: boolean) { super(); Object.freeze(this); } - validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike { + validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike>>{ if (isNullOrUndefined(value)) { return ctx.failurePromise(defaultViolations.notNull(path), value); } @@ -900,7 +896,7 @@ export class MapValidator extends Validator { return ctx.failurePromise(new TypeMismatch(path, 'Map'), value); } const map: Map = value as Map; - const convertedMap: Map = this.jsonSafeMap ? new JsonMap() : new Map(); + const convertedMap: Map = this.jsonSafeMap ? new JsonMap() : new Map(); const promises: Promise[] = []; let violations: Violation[] = []; let i = 0; @@ -909,8 +905,8 @@ export class MapValidator extends Validator { const keyPromise = this.keys.validatePath(key, entryPath.index(0), ctx); const valuePromise = this.values.validatePath(value, entryPath.index(1), ctx); promises[i] = Promise.all([keyPromise, valuePromise]).then(results => { - const keyResult = results[0] as ValidationResult; - const valueResult = results[1] as ValidationResult; + const keyResult = results[0]; + const valueResult = results[1]; const keySuccess = handleResult(keyResult); const valueSuccess = handleResult(valueResult); if (keySuccess && valueSuccess) { @@ -937,11 +933,11 @@ export class MapValidator extends Validator { } } -export class MapNormalizer extends MapValidator { - constructor(keys: Validator, values: Validator) { - super(keys, values, true); +export class MapNormalizer extends MapValidator { + constructor(keys: Validator, values: Validator, jsonSafeMap: boolean = true) { + super(keys, values, jsonSafeMap); } - validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike { + validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike>> { if (value instanceof Map) { return super.validatePath(value, path, ctx); } @@ -978,12 +974,12 @@ export class JsonMap extends Map { } } -export class SetValidator extends Validator { +export class SetValidator extends Validator> { constructor(public readonly values: Validator, public readonly jsonSafeSet: boolean) { super(); Object.freeze(this); } - validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike { + validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike>> { if (isNullOrUndefined(value)) { return ctx.failurePromise(defaultViolations.notNull(path), value); } @@ -1024,8 +1020,8 @@ export class JsonSet extends Set { } } -export class AnyValidator extends Validator { - validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike { +export class AnyValidator extends Validator { + validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike> { return ctx.successPromise(value); } } @@ -1039,25 +1035,25 @@ export function isSimplePrimitive(value: any) { return type === 'boolean' || type === 'number' || type === 'bigint' || type === 'string' || type === 'symbol'; } -export class StringValidator extends Validator { - validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike { +export class StringValidator extends Validator { + validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike> { if (isNullOrUndefined(value)) { return ctx.failurePromise(defaultViolations.notNull(path), value); } if (isString(value)) { - return ctx.successPromise(value); + return ctx.successPromise(value.toString()); } return ctx.failurePromise(defaultViolations.string(value, path), value); } } -export class StringNormalizer extends Validator { - validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike { +export class StringNormalizer extends Validator { + validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike> { if (isNullOrUndefined(value)) { return ctx.failurePromise(defaultViolations.notNull(path), value); } if (isString(value)) { - return ctx.successPromise(value); + return ctx.successPromise(value.toString()); } if (isSimplePrimitive(value)) { return ctx.successPromise(String(value)); @@ -1066,14 +1062,14 @@ export class StringNormalizer extends Validator { } } -export class NotNullOrUndefinedValidator extends Validator { - validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike { +export class NotNullOrUndefinedValidator extends Validator { + validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike> { return isNullOrUndefined(value) ? ctx.failurePromise(defaultViolations.notNull(path), value) : ctx.successPromise(value); } } -export class IsNullOrUndefinedValidator extends Validator { - validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike { +export class IsNullOrUndefinedValidator extends Validator { + validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike> { return isNullOrUndefined(value) ? ctx.successPromise(value) : ctx.failurePromise(new TypeMismatch(path, 'NullOrUndefined', value), value); } } @@ -1086,7 +1082,7 @@ export class NotEmptyValidator extends Validator { } } -export class SizeValidator extends Validator { +export class SizeValidator extends Validator { constructor(private readonly min: number, private readonly max: number) { super(); if (max < min) { @@ -1095,7 +1091,7 @@ export class SizeValidator extends Validator { Object.freeze(this); } - validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike { + validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike> { if (isNullOrUndefined(value)) { return ctx.failurePromise(defaultViolations.notNull(path), value); } @@ -1125,8 +1121,8 @@ export class NotBlankValidator extends Validator { } } -export class BooleanValidator extends Validator { - validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike { +export class BooleanValidator extends Validator { + validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike> { if (isNullOrUndefined(value)) { return ctx.failurePromise(defaultViolations.notNull(path), value); } @@ -1137,7 +1133,7 @@ export class BooleanValidator extends Validator { } } -export class BooleanNormalizer extends Validator { +export class BooleanNormalizer extends Validator { constructor(public readonly truePattern: RegExp, public readonly falsePattern: RegExp) { super(); Object.freeze(this.truePattern); @@ -1145,7 +1141,7 @@ export class BooleanNormalizer extends Validator { Object.freeze(this); } - validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike { + validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike> { if (isNullOrUndefined(value)) { return ctx.failurePromise(defaultViolations.notNull(path), value); } @@ -1156,10 +1152,11 @@ export class BooleanNormalizer extends Validator { return ctx.successPromise(value.valueOf()); } if (isString(value)) { - if (this.truePattern.test(value)) { + const str = value.toString() + if (this.truePattern.test(str)) { return ctx.successPromise(true); } - if (this.falsePattern.test(value)) { + if (this.falsePattern.test(str)) { return ctx.successPromise(false); } return ctx.failurePromise(defaultViolations.boolean(value, path), value); @@ -1179,13 +1176,13 @@ export function isNumber(value: any) { return (typeof value === 'number' || value instanceof Number) && !Number.isNaN(value.valueOf()); } -export class NumberValidator extends Validator { +export class NumberValidator extends Validator { constructor(public readonly format: NumberFormat) { super(); Object.freeze(this); } - validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike { + validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike> { if (isNullOrUndefined(value)) { return ctx.failurePromise(defaultViolations.notNull(path), value); } @@ -1211,7 +1208,7 @@ export class NumberNormalizer extends NumberValidator { super(format); } - validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike { + validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike> { if (isNullOrUndefined(value)) { return ctx.failurePromise(defaultViolations.notNull(path), value); } @@ -1232,13 +1229,13 @@ export class NumberNormalizer extends NumberValidator { } } -export class MinValidator extends Validator { +export class MinValidator extends Validator { constructor(public readonly min: number, public readonly inclusive: boolean) { super(); Object.freeze(this); } - validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike { + validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike> { if (isNullOrUndefined(value)) { return ctx.failurePromise(defaultViolations.notNull(path), value); } @@ -1256,13 +1253,13 @@ export class MinValidator extends Validator { } } -export class MaxValidator extends Validator { +export class MaxValidator extends Validator { constructor(public readonly max: number, public readonly inclusive: boolean) { super(); Object.freeze(this); } - validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike { + validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike> { if (isNullOrUndefined(value)) { return ctx.failurePromise(defaultViolations.notNull(path), value); } @@ -1280,13 +1277,13 @@ export class MaxValidator extends Validator { } } -export class EnumValidator extends Validator { - constructor(public readonly enumType: object, public readonly name: string) { +export class EnumValidator extends Validator { + constructor(public readonly enumType: T, public readonly name: string) { super(); Object.freeze(this); } - validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike { + validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike> { if (isNullOrUndefined(value)) { return ctx.failurePromise(defaultViolations.notNull(path), value); } @@ -1298,16 +1295,13 @@ export class EnumValidator extends Validator { } } -export class AssertTrueValidator extends Validator { - public fn: AssertTrue; - - constructor(fn: AssertTrue, public readonly type: string, public readonly path?: Path) { +export class AssertTrueValidator extends Validator { + constructor(public readonly fn: AssertTrue, public readonly type: string, public readonly path?: Path) { super(); - this.fn = fn; Object.freeze(this); } - validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike { + validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike> { if (!this.fn(value, path, ctx)) { return ctx.failurePromise(new Violation(this.path ? this.path.connectTo(path) : path, this.type), value); } @@ -1315,33 +1309,34 @@ export class AssertTrueValidator extends Validator { } } -export class UuidValidator extends Validator { +export class UuidValidator extends Validator { constructor(public readonly version?: number) { super(); } - validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike { + validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike> { if (isNullOrUndefined(value)) { return ctx.failurePromise(defaultViolations.notNull(path), value); } if (!isString(value)) { return ctx.failurePromise(defaultViolations.string(value, path), value); } - if (!uuidValidate(value)) { + const str = value.toString(); + if (!uuidValidate(str)) { return ctx.failurePromise(new Violation(path, 'UUID', value), value); } - if (this.version && uuidVersion(value) !== this.version) { + if (this.version && uuidVersion(str) !== this.version) { return ctx.failurePromise(new Violation(path, `UUIDv${this.version}`, value), value); } - return ctx.successPromise(value); + return ctx.successPromise(str); } } -export class HasValueValidator extends Validator { - constructor(public readonly expectedValue: any) { +export class HasValueValidator extends Validator { + constructor(public readonly expectedValue: T) { super(); Object.freeze(this); } - validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike { + validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike> { if (deepEqual(value, this.expectedValue)) { return ctx.successPromise(value); } @@ -1349,25 +1344,9 @@ export class HasValueValidator extends Validator { } } -export function maybeAllOfValidator(validatorOrArray: Validator | Validator[]): Validator { - if (Array.isArray(validatorOrArray)) { - if (validatorOrArray.length === 0) { - return new AnyValidator(); - } - if (validatorOrArray.length === 1) { - return validatorOrArray[0]; - } - return new AllOfValidator(validatorOrArray); - } - return validatorOrArray as Validator; -} - export class AllOfValidator extends Validator { - public readonly validators: Validator[]; - - constructor(validators: Validator[]) { + constructor(public readonly validators: [Validator, ...Validator[]]) { super(); - this.validators = ([] as Validator[]).concat(validators); Object.freeze(this.validators); Object.freeze(this); } @@ -1401,13 +1380,13 @@ export class AllOfValidator extends Validator { } } -export class DateValidator extends Validator { +export class DateValidator extends Validator { constructor(public readonly dateType: string) { super(); Object.freeze(this); } - validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike { + validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike> { if (isNullOrUndefined(value)) { return ctx.failurePromise(defaultViolations.notNull(path), value); } @@ -1427,7 +1406,7 @@ export class DateValidator extends Validator { } } -export class PatternValidator extends Validator { +export class PatternValidator extends Validator { public readonly regExp: RegExp; constructor(pattern: string | RegExp, flags?: string) { @@ -1437,15 +1416,16 @@ export class PatternValidator extends Validator { Object.freeze(this); } - validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike { + validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike> { if (isNullOrUndefined(value)) { return ctx.failurePromise(defaultViolations.notNull(path), value); } if (!isString(value)) { return ctx.failurePromise(defaultViolations.string(value, path), value); } - if (this.regExp.test(value)) { - return ctx.successPromise(value); + const str = value.toString(); + if (this.regExp.test(str)) { + return ctx.successPromise(str); } return ctx.failurePromise(defaultViolations.pattern(this.regExp, value, path), value); } @@ -1461,7 +1441,7 @@ export class PatternNormalizer extends PatternValidator { constructor(pattern: string | RegExp, flags?: string) { super(pattern, flags); } - validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike { + validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike> { if (isNullOrUndefined(value)) { return ctx.failurePromise(defaultViolations.notNull(path), value); } @@ -1475,20 +1455,13 @@ export class PatternNormalizer extends PatternValidator { } } -export class OptionalValidator extends Validator { - private readonly validator: Validator; - - constructor(type: Validator, allOf: Validator[]) { +export class OptionalValidator extends Validator { + constructor(private readonly validator: Validator) { super(); - if (allOf && allOf.length > 0) { - this.validator = new NextValidator(type, maybeAllOfValidator(allOf)); - } else { - this.validator = type; - } Object.freeze(this); } - validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike { + validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike> { if (isNullOrUndefined(value)) { return ctx.successPromise(value); } @@ -1496,20 +1469,13 @@ export class OptionalValidator extends Validator { } } -export class RequiredValidator extends Validator { - private readonly validator: Validator; - - constructor(type: Validator, allOf: Validator[]) { +export class RequiredValidator extends Validator { + constructor(private readonly validator: Validator) { super(); - if (allOf && allOf.length > 0) { - this.validator = new NextValidator(type, maybeAllOfValidator(allOf)); - } else { - this.validator = type; - } Object.freeze(this); } - validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike { + validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike> { if (isNullOrUndefined(value)) { return ctx.failurePromise(defaultViolations.notNull(path), value); } @@ -1517,13 +1483,13 @@ export class RequiredValidator extends Validator { } } -export class ValueMapper extends Validator { - constructor(public readonly fn: MappingFn, public readonly error?: any) { +export class ValueMapper extends Validator { + constructor(public readonly fn: MappingFn, public readonly error?: any) { super(); Object.freeze(this); } - validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike { + validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike> { try { const maybePromise = this.fn(value, path, ctx); if (isPromise(maybePromise)) { @@ -1554,12 +1520,12 @@ export class ValueMapper extends Validator { } } -export function isPromise(value: any) { +export function isPromise(value: any): value is PromiseLike { return value && typeof value['then'] === 'function'; } -export class IgnoreValidator extends Validator { - validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike { +export class IgnoreValidator extends Validator { + validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike> { return ctx.successPromise(undefined); } } @@ -1574,16 +1540,13 @@ const allowNoneMapEntries: MapEntryValidator = new MapEntryValidator({ values: strictUnknownPropertyValidator, }); -export class JsonValidator extends Validator { - private readonly validator: Validator; - - constructor(allOf: Validator[]) { +export class JsonValidator extends Validator { + constructor(private readonly validator: Validator) { super(); - this.validator = maybeAllOfValidator(allOf); Object.freeze(this); } - validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike { + validatePath(value: any, path: Path, ctx: ValidationContext): PromiseLike> { if (isNullOrUndefined(value)) { return ctx.failurePromise(defaultViolations.notNull(path), value); } @@ -1591,7 +1554,7 @@ export class JsonValidator extends Validator { return ctx.failurePromise(defaultViolations.string(value, path), value); } try { - const parsedValue = JSON.parse(value); + const parsedValue = JSON.parse(value.toString()); return this.validator.validatePath(parsedValue, path, ctx); } catch (e) { return ctx.failurePromise(new TypeMismatch(path, 'JSON', value), value);