Skip to content

Commit

Permalink
feature: Immutable validators API
Browse files Browse the repository at this point in the history
  • Loading branch information
vbudovski committed Jul 15, 2024
1 parent 2e53e55 commit 22e11e4
Show file tree
Hide file tree
Showing 23 changed files with 361 additions and 126 deletions.
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,16 @@ of this library.

## Acknowledgements

### [Zod](https://github.com/colinhacks/zod)
### Zod

This library wouldn't exist without Zod as a source of inspiration, with its incredibly expressive, and straightforward
API. Zod is an excellent, and very mature library, and if the highest possible performance isn't a key requirement,
then it is a great choice.
This library wouldn't exist without [Zod](https://github.com/colinhacks/zod) as a source of inspiration, with its incredibly expressive, and
straightforward API. Zod is an excellent, and very mature library, and if the highest possible performance isn't a key
requirement, then it is a great choice.

### [Valita](https://github.com/badrap/valita)
### Valita

It sets an incredibly high bar for parsing performance, and is the current benchmark for this implementation. Some of
the goals of this library differ from the goals of Valita, but it is nonetheless an excellent project.
the goals of this library differ from the goals of [Valita](https://github.com/badrap/valita), but it is nonetheless an excellent project.

## Goals

Expand All @@ -34,6 +34,7 @@ The list may be expanded in time, but for now the objectives are the following:
* High performance[^2], and usability in a strict
[Content Security Policy (CSP)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) environment.
* An API that is *reasonably* close to that of Zod. One-to-one compatibility is not the intention.
* Immutability of schemas. This avoids a lot of bugs caused by mutating references to non-primitive types.

## Documentation

Expand Down
7 changes: 4 additions & 3 deletions paseri-docs/src/content/docs/guides/introduction.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ of this library.

### Zod

This library wouldn't exist without [Zod](https://github.com/colinhacks/zod) as a source of inspiration, with its incredibly expressive, and straightforward
API. Zod is an excellent, and very mature library, and if the highest possible performance isn't a key requirement,
then it is a great choice.
This library wouldn't exist without [Zod](https://github.com/colinhacks/zod) as a source of inspiration, with its incredibly expressive, and
straightforward API. Zod is an excellent, and very mature library, and if the highest possible performance isn't a key
requirement, then it is a great choice.

### Valita

Expand All @@ -31,6 +31,7 @@ The list may be expanded in time, but for now the objectives are the following:
* High performance[^2], and usability in a strict
[Content Security Policy (CSP)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) environment.
* An API that is *reasonably* close to that of Zod. One-to-one compatibility is not the intention.
* Immutability of schemas. This avoids a lot of bugs caused by mutating references to non-primitive types.

---

Expand Down
20 changes: 20 additions & 0 deletions paseri-lib/src/schemas/array.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,23 @@ test('Nullable', () => {
expect(result.ok).toBeTruthy();
}
});

test('Immutable', async (t) => {
await t.step('min', () => {
const original = p.array(p.string());
const modified = original.min(3);
expect(modified).not.toEqual(original);
});

await t.step('max', () => {
const original = p.array(p.string());
const modified = original.max(3);
expect(modified).not.toEqual(original);
});

await t.step('length', () => {
const original = p.array(p.string());
const modified = original.length(3);
expect(modified).not.toEqual(original);
});
});
24 changes: 17 additions & 7 deletions paseri-lib/src/schemas/array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ class ArraySchema<ElementSchemaType extends AnySchemaType> extends Schema<Infer<

this._element = element;
}
protected _clone() {
const cloned = new ArraySchema(this._element);
cloned._minLength = this._minLength;
cloned._maxLength = this._maxLength;

return cloned;
}
_parse(value: unknown): InternalParseResult<Infer<ElementSchemaType[]>> {
if (!Array.isArray(value)) {
return this.issues.INVALID_TYPE;
Expand Down Expand Up @@ -55,20 +62,23 @@ class ArraySchema<ElementSchemaType extends AnySchemaType> extends Schema<Infer<
return undefined;
}
min(length: number) {
this._minLength = length;
const cloned = this._clone();
cloned._minLength = length;

return this;
return cloned;
}
max(length: number) {
this._maxLength = length;
const cloned = this._clone();
cloned._maxLength = length;

return this;
return cloned;
}
length(length: number) {
this._minLength = length;
this._maxLength = length;
const cloned = this._clone();
cloned._minLength = length;
cloned._maxLength = length;

return this;
return cloned;
}
}

Expand Down
26 changes: 26 additions & 0 deletions paseri-lib/src/schemas/bigint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,29 @@ test('Nullable', () => {
expect(result.ok).toBeTruthy();
}
});

test('Immutable', async (t) => {
await t.step('gte', () => {
const original = p.bigint();
const modified = original.gte(3n);
expect(modified).not.toEqual(original);
});

await t.step('gt', () => {
const original = p.bigint();
const modified = original.gt(3n);
expect(modified).not.toEqual(original);
});

await t.step('lte', () => {
const original = p.bigint();
const modified = original.lte(3n);
expect(modified).not.toEqual(original);
});

await t.step('lt', () => {
const original = p.bigint();
const modified = original.lt(3n);
expect(modified).not.toEqual(original);
});
});
53 changes: 32 additions & 21 deletions paseri-lib/src/schemas/bigint.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,31 @@
import type { TreeNode } from '../issue.ts';
import type { InternalParseResult } from '../result.ts';
import { Schema } from './schema.ts';

type CheckFunction = (value: bigint) => TreeNode | undefined;

class BigIntSchema extends Schema<bigint> {
private _checks: CheckFunction[] | undefined = undefined;

readonly issues = {
INVALID_TYPE: { type: 'leaf', code: 'invalid_type' },
TOO_SMALL: { type: 'leaf', code: 'too_small' },
TOO_LARGE: { type: 'leaf', code: 'too_large' },
} as const;

protected _clone() {
const cloned = new BigIntSchema();
cloned._checks = this._checks?.slice();

return cloned;
}
_parse(value: unknown): InternalParseResult<bigint> {
if (typeof value !== 'bigint') {
return this.issues.INVALID_TYPE;
}

if (this.checks !== undefined) {
const length = this.checks.length;
for (let i = 0; i < length; i++) {
const check = this.checks[i];
if (this._checks !== undefined) {
for (const check of this._checks) {
const issue = check(value);
if (issue) {
return issue;
Expand All @@ -27,53 +36,55 @@ class BigIntSchema extends Schema<bigint> {
return undefined;
}
gte(value: bigint) {
this.addCheck((_value) => {
const cloned = this._clone();
cloned._checks = this._checks || [];
cloned._checks.push((_value) => {
if (_value < value) {
return this.issues.TOO_SMALL;
}

return undefined;
});

return this;
return cloned;
}
gt(value: bigint) {
this.addCheck((_value) => {
const cloned = this._clone();
cloned._checks = this._checks || [];
cloned._checks.push((_value) => {
if (_value <= value) {
return this.issues.TOO_SMALL;
}

return undefined;
});

return this;
return cloned;
}
lte(value: bigint) {
this.addCheck((_value) => {
const cloned = this._clone();
cloned._checks = this._checks || [];
cloned._checks.push((_value) => {
if (_value > value) {
return this.issues.TOO_LARGE;
}

return undefined;
});

return this;
return cloned;
}
lt(value: bigint) {
this.addCheck((_value) => {
const cloned = this._clone();
cloned._checks = this._checks || [];
cloned._checks.push((_value) => {
if (_value >= value) {
return this.issues.TOO_LARGE;
}

return undefined;
});

return this;
return cloned;
}
}

const singleton = new BigIntSchema();

function bigint() {
return new BigIntSchema();
return singleton;
}

export { bigint };
7 changes: 6 additions & 1 deletion paseri-lib/src/schemas/boolean.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ class BooleanSchema extends Schema<boolean> {
INVALID_TYPE: { type: 'leaf', code: 'invalid_type' },
} as const;

protected _clone() {
return new BooleanSchema();
}
_parse(value: unknown): InternalParseResult<boolean> {
if (typeof value !== 'boolean') {
return this.issues.INVALID_TYPE;
Expand All @@ -15,8 +18,10 @@ class BooleanSchema extends Schema<boolean> {
}
}

const singleton = new BooleanSchema();

function boolean() {
return new BooleanSchema();
return singleton;
}

export { boolean };
5 changes: 4 additions & 1 deletion paseri-lib/src/schemas/literal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Schema } from './schema.ts';
type LiteralType = string | number | bigint | boolean | symbol;

class LiteralSchema<OutputType extends LiteralType> extends Schema<OutputType> {
private readonly _value: LiteralType;
private readonly _value: OutputType;

readonly issues = {
INVALID_VALUE: { type: 'leaf', code: 'invalid_value' },
Expand All @@ -16,6 +16,9 @@ class LiteralSchema<OutputType extends LiteralType> extends Schema<OutputType> {

this._value = value;
}
protected _clone() {
return new LiteralSchema(this._value as IsLiteral<OutputType> extends true ? OutputType : never);
}
_parse(value: unknown): InternalParseResult<OutputType> {
if (value !== this._value) {
return this.issues.INVALID_VALUE;
Expand Down
3 changes: 3 additions & 0 deletions paseri-lib/src/schemas/never.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ class NeverSchema extends Schema<never> {
INVALID_TYPE: { type: 'leaf', code: 'invalid_type' },
} as const;

protected _clone() {
return new NeverSchema();
}
_parse(value: unknown): InternalParseResult<never> {
return this.issues.INVALID_TYPE;
}
Expand Down
7 changes: 6 additions & 1 deletion paseri-lib/src/schemas/null.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ class NullSchema extends Schema<null> {
INVALID_VALUE: { type: 'leaf', code: 'invalid_value' },
} as const;

protected _clone() {
return new NullSchema();
}
_parse(value: unknown): InternalParseResult<null> {
if (value !== null) {
return this.issues.INVALID_VALUE;
Expand All @@ -15,9 +18,11 @@ class NullSchema extends Schema<null> {
}
}

const singleton = new NullSchema();

// `null` is a reserved word.
function null_() {
return new NullSchema();
return singleton;
}

export { null_ };
44 changes: 44 additions & 0 deletions paseri-lib/src/schemas/number.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,3 +209,47 @@ test('Nullable', () => {
expect(result.ok).toBeTruthy();
}
});

test('Immutable', async (t) => {
await t.step('gte', () => {
const original = p.number();
const modified = original.gte(3);
expect(modified).not.toEqual(original);
});

await t.step('gt', () => {
const original = p.number();
const modified = original.gt(3);
expect(modified).not.toEqual(original);
});

await t.step('lte', () => {
const original = p.number();
const modified = original.lte(3);
expect(modified).not.toEqual(original);
});

await t.step('lt', () => {
const original = p.number();
const modified = original.lt(3);
expect(modified).not.toEqual(original);
});

await t.step('int', () => {
const original = p.number();
const modified = original.int();
expect(modified).not.toEqual(original);
});

await t.step('finite', () => {
const original = p.number();
const modified = original.finite();
expect(modified).not.toEqual(original);
});

await t.step('safe', () => {
const original = p.number();
const modified = original.safe();
expect(modified).not.toEqual(original);
});
});
Loading

0 comments on commit 22e11e4

Please sign in to comment.