Skip to content

Commit

Permalink
feat: lazy validator (#147)
Browse files Browse the repository at this point in the history
* feat: lazy validator

* fix: types and nested test

* test: add circular test

* chore: add expect error

* chore: peopleware chapter 8: "you never get anything done around here between 9 and 5."

permalink: http://whatthecommit.com/17d9ac5396b751c8be5cceca8c56769f

* chore: merge pull my finger request

permalink: http://whatthecommit.com/569aa58b98f6e4aac466357a4997c2b2

* chore: dismiss 2 approvals

* chore: this is why docs are important

permalink: http://whatthecommit.com/3bdc00b8165a743b7ee0716faf792d4f

Co-authored-by: Jeroen Claassens <support@favware.tech>
  • Loading branch information
imranbarbhuiya and favna authored Jul 9, 2022
1 parent d3751f6 commit 807666e
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 0 deletions.
5 changes: 5 additions & 0 deletions src/lib/Shapes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
TupleValidator,
UnionValidator
} from '../validators/imports';
import { LazyValidator } from '../validators/LazyValidator';
import { NativeEnumLike, NativeEnumValidator } from '../validators/NativeEnumValidator';
import { TypedArrayValidator } from '../validators/TypedArrayValidator';
import type { Constructor, MappedObjectValidator } from './util-types';
Expand Down Expand Up @@ -161,4 +162,8 @@ export class Shapes {
public map<T, U>(keyValidator: BaseValidator<T>, valueValidator: BaseValidator<U>) {
return new MapValidator(keyValidator, valueValidator);
}

public lazy<T extends BaseValidator<unknown>>(validator: (value: unknown) => T) {
return new LazyValidator(validator);
}
}
20 changes: 20 additions & 0 deletions src/validators/LazyValidator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { Result } from '../lib/Result';
import type { IConstraint, Unwrap } from '../type-exports';
import { BaseValidator, ValidatorError } from './imports';

export class LazyValidator<T extends BaseValidator<unknown>, R = Unwrap<T>> extends BaseValidator<R> {
private readonly validator: (value: unknown) => T;

public constructor(validator: (value: unknown) => T, constraints: readonly IConstraint<R>[] = []) {
super(constraints);
this.validator = validator;
}

protected override clone(): this {
return Reflect.construct(this.constructor, [this.validator, this.constraints]);
}

protected handle(values: unknown): Result<R, ValidatorError> {
return this.validator(values).run(values) as Result<R, ValidatorError>;
}
}
75 changes: 75 additions & 0 deletions tests/validators/lazy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { CombinedPropertyError, ExpectedConstraintError, MissingPropertyError, s, SchemaOf, ValidationError } from '../../src';
import { expectError } from '../common/macros/comparators';

describe('LazyValidator', () => {
const predicate = s.lazy((value) => {
if (typeof value === 'boolean') return s.boolean.true;
return s.string;
});

test.each([true, 'hello'])('GIVEN %j THEN returns the given value', (input) => {
expect<true | string>(predicate.parse(input)).toBe(input);
});

test('GIVEN an invalid value THEN throw ValidationError', () => {
expectError(() => predicate.parse(123), new ValidationError('s.string', 'Expected a string primitive', 123));
});
});

describe('NestedLazyValidator', () => {
const predicate = s.lazy((value) => {
if (typeof value === 'boolean') return s.boolean.true;
return s.lazy((value) => {
if (typeof value === 'string') return s.string.lengthEqual(5);
return s.number;
});
});

test.each([true, 'hello', 123])('GIVEN %j THEN returns the given value', (input) => {
expect<true | string | number>(predicate.parse(input)).toBe(input);
});

test('GIVEN an invalid value THEN throw ValidationError', () => {
expectError(
() => predicate.parse('Sapphire'),
new ExpectedConstraintError('s.string.lengthEqual', 'Invalid string length', 'Sapphire', 'expected.length === 5')
);
});
});

describe('CircularLazyValidator', () => {
interface PredicateSchema {
id: string;
items: PredicateSchema;
}

const predicate: SchemaOf<PredicateSchema> = s.object({
id: s.string,
items: s.lazy<SchemaOf<PredicateSchema>>(() => predicate)
});

test('GIVEN circular schema THEN throw ', () => {
expectError(
() => predicate.parse({ id: 'Hello', items: { id: 'Hello', items: { id: 'Hello' } } }),
new CombinedPropertyError([
['items', new CombinedPropertyError([['items', new CombinedPropertyError([['items', new MissingPropertyError('items')]])]])]
])
);
});
});

describe('PassingCircularLazyValidator', () => {
interface PredicateSchema {
id: string;
items?: PredicateSchema;
}

const predicate: SchemaOf<PredicateSchema> = s.object({
id: s.string,
items: s.lazy<SchemaOf<PredicateSchema>>(() => predicate).optional
});

test('GIVEN circular schema THEN return given value', () => {
expect(predicate.parse({ id: 'Sapphire', items: { id: 'Hello' } })).toStrictEqual({ id: 'Sapphire', items: { id: 'Hello' } });
});
});

0 comments on commit 807666e

Please sign in to comment.