Skip to content

Commit

Permalink
Merge pull request #8 from vbudovski/feature/lazy-validator
Browse files Browse the repository at this point in the history
feature: Lazy validator
  • Loading branch information
vbudovski authored Jul 26, 2024
2 parents b561009 + 2d179af commit 31b022f
Show file tree
Hide file tree
Showing 6 changed files with 191 additions and 0 deletions.
54 changes: 54 additions & 0 deletions paseri-docs/src/content/docs/reference/Others/lazy.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
title: "Lazy"
sidebar:
order: 45
---

It's possible to define a recursive schema using lazy evaluation. Unfortunately, due to a limitation of TypeScript, its
type cannot be automatically inferred.

```typescript
import * as p from '@vbudovski/paseri';

type Node =
| { type: 'file'; name: string }
| { type: 'directory'; name: string; nodes: Node[] };

// Give TypeScript a hint about the recursive structure.
const schema: p.Schema<Node> = p.lazy(() =>
p.union(
p.object({
type: p.literal('file'),
name: p.string(),
}),
p.object({
type: p.literal('directory'),
name: p.string(),
nodes: p.array(schema),
}),
),
);
const data: Node = {
type: 'directory',
name: 'Documents',
nodes: [
{
type: 'file',
name: 'shopping_list.txt',
},
{
type: 'directory',
name: 'Pictures',
nodes: [
{ type: 'file', name: 'cat.jpg' },
{ type: 'file', name: 'dog.jpg' },
],
},
],
};

const result = schema.safeParse(data);
if (result.ok) {
// result.value typed as `Node`.
}
```
51 changes: 51 additions & 0 deletions paseri-lib/bench/lazy/type.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import * as v from '@badrap/valita';
import { z } from 'zod';
import * as p from '../../src/index.ts';

const { bench } = Deno;

type T = string | T[];

const paseriSchema: p.Schema<T> = p.lazy(() => p.union(p.string(), p.array(paseriSchema)));
const zodSchema: z.ZodType<T> = z.lazy(() => z.union([z.string(), z.array(zodSchema)]));
const valitaSchema: v.Type<T> = v.lazy(() => v.union(v.string(), v.array(valitaSchema)));

const dataValid1 = 'Hello, world!';
const dataValid2 = ['foo', 'bar'];
const dataValid3 = ['foo', ['bar', 'baz']];

bench('Paseri', { group: 'Type valid 1' }, () => {
paseriSchema.safeParse(dataValid1);
});

bench('Zod', { group: 'Type valid 1' }, () => {
zodSchema.safeParse(dataValid1);
});

bench('Valita', { group: 'Type valid 1' }, () => {
valitaSchema.try(dataValid1);
});

bench('Paseri', { group: 'Type valid 2' }, () => {
paseriSchema.safeParse(dataValid2);
});

bench('Zod', { group: 'Type valid 2' }, () => {
zodSchema.safeParse(dataValid2);
});

bench('Valita', { group: 'Type valid 2' }, () => {
valitaSchema.try(dataValid2);
});

bench('Paseri', { group: 'Type valid 3' }, () => {
paseriSchema.safeParse(dataValid3);
});

bench('Zod', { group: 'Type valid 3' }, () => {
zodSchema.safeParse(dataValid3);
});

bench('Valita', { group: 'Type valid 3' }, () => {
valitaSchema.try(dataValid3);
});
2 changes: 2 additions & 0 deletions paseri-lib/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export {
array,
bigint,
boolean,
lazy,
literal,
map,
never,
Expand All @@ -16,6 +17,7 @@ export {
undefined_ as undefined,
union,
unknown,
Schema,
} from './schemas/index.ts';
export { ok, err } from './result.ts';

Expand Down
3 changes: 3 additions & 0 deletions paseri-lib/src/schemas/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export { array } from './array.ts';
export { bigint } from './bigint.ts';
export { boolean } from './boolean.ts';
export { lazy } from './lazy.ts';
export { literal } from './literal.ts';
export { map } from './map.ts';
export { never } from './never.ts';
Expand All @@ -15,3 +16,5 @@ export { tuple } from './tuple.ts';
export { undefined_ } from './undefined.ts';
export { union } from './union.ts';
export { unknown } from './unknown.ts';

export { Schema } from './schema.ts';
51 changes: 51 additions & 0 deletions paseri-lib/src/schemas/lazy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { expect } from '@std/expect';
import { expectTypeOf } from 'expect-type';
import fc from 'fast-check';
import * as p from '../index.ts';

const { test } = Deno;

test('Valid type', () => {
type T = string | T[];
const schema: p.Schema<T> = p.lazy(() => p.union(p.string(), p.array(schema)));

const { tree } = fc.letrec((tie) => ({
tree: fc.oneof({ depthSize: 'small', withCrossShrink: true }, tie('leaf'), tie('node')),
node: fc.array(tie('tree')),
leaf: fc.string(),
}));

fc.assert(
fc.property(tree, (data) => {
const result = schema.safeParse(data);
if (result.ok) {
expectTypeOf(result.value).toEqualTypeOf<T>;
expect(result.value).toBe(data);
} else {
expect(result.ok).toBeTruthy();
}
}),
);
});

test('Invalid type', () => {
type T = string | T[];
const schema: p.Schema<T> = p.lazy(() => p.union(p.string(), p.array(schema)));

const { tree } = fc.letrec((tie) => ({
tree: fc.oneof({ depthSize: 'small', withCrossShrink: true }, tie('leaf'), tie('node')),
node: fc.array(tie('tree'), { minLength: 1 }),
leaf: fc.anything().filter((value) => !(typeof value === 'string' || Array.isArray(value))),
}));

fc.assert(
fc.property(tree, (data) => {
const result = schema.safeParse(data);
if (!result.ok) {
expect(result.issue).toEqual({ type: 'leaf', code: 'invalid_value' });
} else {
expect(result.ok).toBeFalsy();
}
}),
);
});
30 changes: 30 additions & 0 deletions paseri-lib/src/schemas/lazy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { InternalParseResult } from '../result.ts';
import { Schema } from './schema.ts';

class LazySchema<OutputType> extends Schema<OutputType> {
private readonly _lazy: () => Schema<OutputType>;
private _schema: Schema<OutputType> | undefined;

constructor(lazy: () => Schema<OutputType>) {
super();

this._lazy = lazy;
}
protected _clone(): LazySchema<OutputType> {
return this;
}
_parse(value: unknown): InternalParseResult<OutputType> {
// Evaluate the schema only once for the entire recursive structure.
if (this._schema === undefined) {
this._schema = this._lazy();
}

return this._schema._parse(value);
}
}

function lazy<OutputType>(...args: ConstructorParameters<typeof LazySchema<OutputType>>): LazySchema<OutputType> {
return new LazySchema(...args);
}

export { lazy };

0 comments on commit 31b022f

Please sign in to comment.