From b1083342d31d32dea426f73be3f9192bd7e40b34 Mon Sep 17 00:00:00 2001 From: Vitaly Budovski Date: Mon, 15 Jul 2024 14:56:24 +1000 Subject: [PATCH] test: Use property-based testing --- paseri-lib/deno.json | 1 + paseri-lib/deno.lock | 12 + paseri-lib/src/schemas/array.test.ts | 241 ++++++++------ paseri-lib/src/schemas/bigint.test.ts | 256 +++++++++----- paseri-lib/src/schemas/boolean.test.ts | 88 +++-- paseri-lib/src/schemas/common.test.ts | 21 ++ paseri-lib/src/schemas/literal.test.ts | 39 ++- paseri-lib/src/schemas/never.test.ts | 49 +-- paseri-lib/src/schemas/null.test.ts | 85 +++-- paseri-lib/src/schemas/number.test.ts | 404 ++++++++++++++--------- paseri-lib/src/schemas/object.test.ts | 339 ++++++++++--------- paseri-lib/src/schemas/record.test.ts | 87 +++-- paseri-lib/src/schemas/string.test.ts | 335 +++++++++++-------- paseri-lib/src/schemas/string.ts | 2 +- paseri-lib/src/schemas/tuple.test.ts | 87 +++-- paseri-lib/src/schemas/undefined.test.ts | 82 +++-- paseri-lib/src/schemas/union.test.ts | 108 +++--- paseri-lib/src/schemas/unknown.test.ts | 43 +-- 18 files changed, 1345 insertions(+), 934 deletions(-) create mode 100644 paseri-lib/src/schemas/common.test.ts diff --git a/paseri-lib/deno.json b/paseri-lib/deno.json index 2be7ab2..6adf706 100644 --- a/paseri-lib/deno.json +++ b/paseri-lib/deno.json @@ -6,6 +6,7 @@ "@std/expect": "jsr:@std/expect@^0.224.4", "esbuild": "npm:esbuild@^0.21.4", "expect-type": "npm:expect-type@^0.19.0", + "fast-check": "npm:fast-check@^3.20.0", "type-fest": "npm:type-fest@^4.20.0", "typescript": "npm:typescript@^5.4.5", "zod": "npm:zod@^3.23.8" diff --git a/paseri-lib/deno.lock b/paseri-lib/deno.lock index ace3dc5..63e9cd2 100644 --- a/paseri-lib/deno.lock +++ b/paseri-lib/deno.lock @@ -9,6 +9,7 @@ "npm:@biomejs/biome": "npm:@biomejs/biome@1.8.0", "npm:esbuild@^0.21.4": "npm:esbuild@0.21.4", "npm:expect-type@^0.19.0": "npm:expect-type@0.19.0", + "npm:fast-check@^3.20.0": "npm:fast-check@3.20.0", "npm:type-fest@^4.20.0": "npm:type-fest@4.20.1", "npm:typescript@^5.4.5": "npm:typescript@5.4.5", "npm:zod@^3.23.8": "npm:zod@3.23.8" @@ -204,6 +205,16 @@ "integrity": "sha512-piv9wz3IrAG4Wnk2A+n2VRCHieAyOSxrRLU872Xo6nyn39kYXKDALk4OcqnvLRnFvkz659CnWC8MWZLuuQnoqg==", "dependencies": {} }, + "fast-check@3.20.0": { + "integrity": "sha512-pZIjqLpOZgdSLecec4GKC3Zq5702MZ34upMKxojnNVSWA0K64V3pXOBT1Wdsrc3AphLtzRBbsi8bRWF4TUGmUg==", + "dependencies": { + "pure-rand": "pure-rand@6.1.0" + } + }, + "pure-rand@6.1.0": { + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dependencies": {} + }, "type-fest@4.20.1": { "integrity": "sha512-R6wDsVsoS9xYOpy8vgeBlqpdOyzJ12HNfQhC/aAKWM3YoCV9TtunJzh/QpkMgeDhkoynDcw5f1y+qF9yc/HHyg==", "dependencies": {} @@ -226,6 +237,7 @@ "npm:@biomejs/biome@^1.8.0", "npm:esbuild@^0.21.4", "npm:expect-type@^0.19.0", + "npm:fast-check@^3.20.0", "npm:type-fest@^4.20.0", "npm:typescript@^5.4.5", "npm:zod@^3.23.8" diff --git a/paseri-lib/src/schemas/array.test.ts b/paseri-lib/src/schemas/array.test.ts index 64cabaa..c087a11 100644 --- a/paseri-lib/src/schemas/array.test.ts +++ b/paseri-lib/src/schemas/array.test.ts @@ -1,110 +1,151 @@ import { expect } from '@std/expect'; import { expectTypeOf } from 'expect-type'; +import fc from 'fast-check'; import * as p from '../index.ts'; import type { TreeNode } from '../issue.ts'; const { test } = Deno; -test('Type', async (t) => { +test('Valid type', () => { const schema = p.array(p.number()); - await t.step('Valid', () => { - const result = schema.safeParse([1, 2, 3]); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf; - expect(result.value).toEqual([1, 2, 3]); - } else { - expect(result.ok).toBeTruthy(); - } - }); + fc.assert( + fc.property(fc.array(fc.float()), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf; + expect(result.value).toEqual(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); +}); - await t.step('Invalid', () => { - const result = schema.safeParse(null); - if (!result.ok) { - const expectedResult: TreeNode = { type: 'leaf', code: 'invalid_type' }; - expect(result.issue).toEqual(expectedResult); - } else { - expect(result.ok).toBeFalsy(); - } - }); +test('Invalid type', () => { + const schema = p.string(); + + fc.assert( + fc.property( + fc.anything().filter((value) => Array.isArray(value)), + (data) => { + const result = schema.safeParse(data); + if (!result.ok) { + expect(result.issue).toEqual({ type: 'leaf', code: 'invalid_type' }); + } else { + expect(result.ok).toBeFalsy(); + } + }, + ), + ); }); -test('Min', async (t) => { +test('Valid min', () => { const schema = p.array(p.number()).min(3); - await t.step('Valid', () => { - const result = schema.safeParse([1, 2, 3]); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf; - expect(result.value).toEqual([1, 2, 3]); - } else { - expect(result.ok).toBeTruthy(); - } - }); + fc.assert( + fc.property(fc.array(fc.float(), { minLength: 3 }), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf; + expect(result.value).toBe(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); +}); - await t.step('Too short', () => { - const result = schema.safeParse([1, 2]); - if (!result.ok) { - expect(result.issue).toEqual({ type: 'leaf', code: 'too_short' }); - } else { - expect(result.ok).toBeFalsy(); - } - }); +test('Invalid min', () => { + const schema = p.array(p.number()).min(3); + + fc.assert( + fc.property(fc.array(fc.float(), { maxLength: 2 }), (data) => { + const result = schema.safeParse(data); + if (!result.ok) { + expect(result.issue).toEqual({ type: 'leaf', code: 'too_short' }); + } else { + expect(result.ok).toBeFalsy(); + } + }), + ); }); -test('Max', async (t) => { +test('Valid max', () => { const schema = p.array(p.number()).max(3); - await t.step('Valid', () => { - const result = schema.safeParse([1, 2, 3]); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf; - expect(result.value).toEqual([1, 2, 3]); - } else { - expect(result.ok).toBeTruthy(); - } - }); + fc.assert( + fc.property(fc.array(fc.float(), { maxLength: 3 }), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf; + expect(result.value).toBe(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); +}); - await t.step('Too long', () => { - const result = schema.safeParse([1, 2, 3, 4]); - if (!result.ok) { - expect(result.issue).toEqual({ type: 'leaf', code: 'too_long' }); - } else { - expect(result.ok).toBeFalsy(); - } - }); +test('Invalid max', () => { + const schema = p.array(p.number()).max(3); + + fc.assert( + fc.property(fc.array(fc.float(), { minLength: 4 }), (data) => { + const result = schema.safeParse(data); + if (!result.ok) { + expect(result.issue).toEqual({ type: 'leaf', code: 'too_long' }); + } else { + expect(result.ok).toBeFalsy(); + } + }), + ); }); -test('Length', async (t) => { +test('Valid length', () => { const schema = p.array(p.number()).length(3); - await t.step('Valid', () => { - const result = schema.safeParse([1, 2, 3]); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf; - expect(result.value).toEqual([1, 2, 3]); - } else { - expect(result.ok).toBeTruthy(); - } - }); + fc.assert( + fc.property(fc.array(fc.float(), { minLength: 3, maxLength: 3 }), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf; + expect(result.value).toBe(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); +}); - await t.step('Too long', () => { - const result = schema.safeParse([1, 2, 3, 4]); - if (!result.ok) { - expect(result.issue).toEqual({ type: 'leaf', code: 'too_long' }); - } else { - expect(result.ok).toBeFalsy(); - } - }); +test('Invalid length (too long)', () => { + const schema = p.array(p.number()).length(3); - await t.step('Too short', () => { - const result = schema.safeParse([1, 2]); - if (!result.ok) { - expect(result.issue).toEqual({ type: 'leaf', code: 'too_short' }); - } else { - expect(result.ok).toBeFalsy(); - } - }); + fc.assert( + fc.property(fc.array(fc.float(), { minLength: 4 }), (data) => { + const result = schema.safeParse(data); + if (!result.ok) { + expect(result.issue).toEqual({ type: 'leaf', code: 'too_long' }); + } else { + expect(result.ok).toBeFalsy(); + } + }), + ); +}); + +test('Invalid length (too short)', () => { + const schema = p.array(p.number()).length(3); + + fc.assert( + fc.property(fc.array(fc.float(), { maxLength: 2 }), (data) => { + const result = schema.safeParse(data); + if (!result.ok) { + expect(result.issue).toEqual({ type: 'leaf', code: 'too_short' }); + } else { + expect(result.ok).toBeFalsy(); + } + }), + ); }); test('Invalid elements', () => { @@ -126,24 +167,34 @@ test('Invalid elements', () => { test('Optional', () => { const schema = p.array(p.number()).optional(); - const result = schema.safeParse(undefined); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf; - expect(result.value).toBe(undefined); - } else { - expect(result.ok).toBeTruthy(); - } + + fc.assert( + fc.property(fc.option(fc.array(fc.float()), { nil: undefined }), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf; + expect(result.value).toEqual(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); }); test('Nullable', () => { const schema = p.array(p.number()).nullable(); - const result = schema.safeParse(null); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf; - expect(result.value).toBe(null); - } else { - expect(result.ok).toBeTruthy(); - } + + fc.assert( + fc.property(fc.option(fc.array(fc.float()), { nil: null }), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf; + expect(result.value).toEqual(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); }); test('Immutable', async (t) => { diff --git a/paseri-lib/src/schemas/bigint.test.ts b/paseri-lib/src/schemas/bigint.test.ts index 3a66341..9c4385b 100644 --- a/paseri-lib/src/schemas/bigint.test.ts +++ b/paseri-lib/src/schemas/bigint.test.ts @@ -1,122 +1,198 @@ 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('Type', async (t) => { +test('Valid type', () => { const schema = p.bigint(); - await t.step('Valid', () => { - const result = schema.safeParse(123n); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf; - expect(result.value).toBe(123n); - } else { - expect(result.ok).toBeTruthy(); - } - }); + fc.assert( + fc.property(fc.bigInt(), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf; + expect(result.value).toBe(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); +}); - await t.step('Not a number', () => { - const result = schema.safeParse(null); - if (!result.ok) { - expect(result.issue).toEqual({ type: 'leaf', code: 'invalid_type' }); - } else { - expect(result.ok).toBeFalsy(); - } - }); +test('Invalid type', () => { + const schema = p.bigint(); + + fc.assert( + fc.property( + fc.anything().filter((value) => typeof value !== 'bigint'), + (data) => { + const result = schema.safeParse(data); + if (!result.ok) { + expect(result.issue).toEqual({ type: 'leaf', code: 'invalid_type' }); + } else { + expect(result.ok).toBeFalsy(); + } + }, + ), + ); }); -test('Greater than or equal', async (t) => { +test('Valid gte', () => { const schema = p.bigint().gte(10n); - await t.step('Valid', () => { - const result = schema.safeParse(10n); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf; - expect(result.value).toBe(10n); - } else { - expect(result.ok).toBeTruthy(); - } - }); + fc.assert( + fc.property(fc.bigInt({ min: 10n }), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf; + expect(result.value).toBe(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); +}); - await t.step('Too small', () => { - const result = schema.safeParse(9n); - if (!result.ok) { - expect(result.issue).toEqual({ type: 'leaf', code: 'too_small' }); - } else { - expect(result.ok).toBeFalsy(); - } - }); +test('Invalid gte', () => { + const schema = p.bigint().gte(10n); + + fc.assert( + fc.property(fc.bigInt({ max: 9n }), (data) => { + const result = schema.safeParse(data); + if (!result.ok) { + expect(result.issue).toEqual({ type: 'leaf', code: 'too_small' }); + } else { + expect(result.ok).toBeFalsy(); + } + }), + ); }); -test('Greater than', async (t) => { +test('Valid gt', () => { const schema = p.bigint().gt(10n); - await t.step('Valid', () => { - const result = schema.safeParse(11n); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf; - expect(result.value).toBe(11n); - } else { - expect(result.ok).toBeTruthy(); - } - }); + fc.assert( + fc.property(fc.bigInt({ min: 11n }), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf; + expect(result.value).toBe(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); +}); - await t.step('Too small', () => { - const result = schema.safeParse(10n); - if (!result.ok) { - expect(result.issue).toEqual({ type: 'leaf', code: 'too_small' }); - } else { - expect(result.ok).toBeFalsy(); - } - }); +test('Invalid gt', () => { + const schema = p.bigint().gt(10n); + + fc.assert( + fc.property(fc.bigInt({ max: 10n }), (data) => { + const result = schema.safeParse(data); + if (!result.ok) { + expect(result.issue).toEqual({ type: 'leaf', code: 'too_small' }); + } else { + expect(result.ok).toBeFalsy(); + } + }), + ); }); -test('Less than or equal', async (t) => { +test('Valid lte', () => { const schema = p.bigint().lte(10n); - await t.step('Valid', () => { - const result = schema.safeParse(10n); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf; - expect(result.value).toBe(10n); - } else { - expect(result.ok).toBeTruthy(); - } - }); + fc.assert( + fc.property(fc.bigInt({ max: 10n }), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf; + expect(result.value).toBe(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); +}); - await t.step('Too large', () => { - const result = schema.safeParse(11n); - if (!result.ok) { - expect(result.issue).toEqual({ type: 'leaf', code: 'too_large' }); - } else { - expect(result.ok).toBeFalsy(); - } - }); +test('Invalid lte', () => { + const schema = p.bigint().lte(10n); + + fc.assert( + fc.property(fc.bigInt({ min: 11n }), (data) => { + const result = schema.safeParse(data); + if (!result.ok) { + expect(result.issue).toEqual({ type: 'leaf', code: 'too_large' }); + } else { + expect(result.ok).toBeFalsy(); + } + }), + ); }); -test('Less than', async (t) => { +test('Valid lt', () => { const schema = p.bigint().lt(10n); - await t.step('Valid', () => { - const result = schema.safeParse(9n); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf; - expect(result.value).toBe(9n); - } else { - expect(result.ok).toBeTruthy(); - } - }); + fc.assert( + fc.property(fc.bigInt({ max: 9n }), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf; + expect(result.value).toBe(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); +}); - await t.step('Too large', () => { - const result = schema.safeParse(10n); - if (!result.ok) { - expect(result.issue).toEqual({ type: 'leaf', code: 'too_large' }); - } else { - expect(result.ok).toBeFalsy(); - } - }); +test('Invalid lt', () => { + const schema = p.bigint().lt(10n); + + fc.assert( + fc.property(fc.bigInt({ min: 10n }), (data) => { + const result = schema.safeParse(data); + if (!result.ok) { + expect(result.issue).toEqual({ type: 'leaf', code: 'too_large' }); + } else { + expect(result.ok).toBeFalsy(); + } + }), + ); +}); + +test('Optional', () => { + const schema = p.bigint().optional(); + + fc.assert( + fc.property(fc.option(fc.bigInt(), { nil: undefined }), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf; + expect(result.value).toEqual(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); +}); + +test('Nullable', () => { + const schema = p.bigint().nullable(); + + fc.assert( + fc.property(fc.option(fc.bigInt(), { nil: null }), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf; + expect(result.value).toEqual(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); }); test('Optional', () => { diff --git a/paseri-lib/src/schemas/boolean.test.ts b/paseri-lib/src/schemas/boolean.test.ts index ab2634a..ec66c28 100644 --- a/paseri-lib/src/schemas/boolean.test.ts +++ b/paseri-lib/src/schemas/boolean.test.ts @@ -1,50 +1,72 @@ 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('Type', async (t) => { +test('Valid type', () => { const schema = p.boolean(); - await t.step('Valid', () => { - const result = schema.safeParse(true); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf; - expect(result.value).toBe(true); - } else { - expect(result.ok).toBeTruthy(); - } - }); - - await t.step('Not a boolean', () => { - const result = schema.safeParse(null); - if (!result.ok) { - expect(result.issue).toEqual({ type: 'leaf', code: 'invalid_type' }); - } else { - expect(result.ok).toBeFalsy(); - } - }); + fc.assert( + fc.property(fc.boolean(), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf; + expect(result.value).toBe(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); +}); + +test('Invalid type', () => { + const schema = p.boolean(); + + fc.assert( + fc.property( + fc.anything().filter((value) => typeof value !== 'boolean'), + (data) => { + const result = schema.safeParse(data); + if (!result.ok) { + expect(result.issue).toEqual({ type: 'leaf', code: 'invalid_type' }); + } else { + expect(result.ok).toBeFalsy(); + } + }, + ), + ); }); test('Optional', () => { const schema = p.boolean().optional(); - const result = schema.safeParse(undefined); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf; - expect(result.value).toBe(undefined); - } else { - expect(result.ok).toBeTruthy(); - } + + fc.assert( + fc.property(fc.option(fc.boolean(), { nil: undefined }), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf; + expect(result.value).toEqual(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); }); test('Nullable', () => { const schema = p.boolean().nullable(); - const result = schema.safeParse(null); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf; - expect(result.value).toBe(null); - } else { - expect(result.ok).toBeTruthy(); - } + + fc.assert( + fc.property(fc.option(fc.boolean(), { nil: null }), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf; + expect(result.value).toEqual(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); }); diff --git a/paseri-lib/src/schemas/common.test.ts b/paseri-lib/src/schemas/common.test.ts new file mode 100644 index 0000000..cbc62cf --- /dev/null +++ b/paseri-lib/src/schemas/common.test.ts @@ -0,0 +1,21 @@ +import { expect } from '@std/expect'; +import * as p from '../index.ts'; + +const { test } = Deno; + +test('Parse fail', () => { + const schema = p.string(); + expect(() => { + schema.parse(123); + }).toThrow('Failed to parse {"type":"leaf","code":"invalid_type"}.'); +}); + +test('Safe parse fail', () => { + const schema = p.string(); + const result = schema.safeParse(123); + if (!result.ok) { + expect(result.issue).toEqual({ type: 'leaf', code: 'invalid_type' }); + } else { + expect(result.ok).toBeFalsy(); + } +}); diff --git a/paseri-lib/src/schemas/literal.test.ts b/paseri-lib/src/schemas/literal.test.ts index 6d5e067..8c24674 100644 --- a/paseri-lib/src/schemas/literal.test.ts +++ b/paseri-lib/src/schemas/literal.test.ts @@ -1,5 +1,6 @@ import { expect } from '@std/expect'; import { expectTypeOf } from 'expect-type'; +import fc from 'fast-check'; import * as p from '../index.ts'; const { test } = Deno; @@ -126,22 +127,32 @@ test('Symbol', async (t) => { test('Optional', () => { const schema = p.literal('apple').optional(); - const result = schema.safeParse(undefined); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf<'apple' | undefined>; - expect(result.value).toBe(undefined); - } else { - expect(result.ok).toBeTruthy(); - } + + fc.assert( + fc.property(fc.option(fc.constant('apple'), { nil: undefined }), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf<'apple' | undefined>; + expect(result.value).toEqual(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); }); test('Nullable', () => { const schema = p.literal('apple').nullable(); - const result = schema.safeParse(null); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf<'apple' | null>; - expect(result.value).toBe(null); - } else { - expect(result.ok).toBeTruthy(); - } + + fc.assert( + fc.property(fc.option(fc.constant('apple'), { nil: null }), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf<'apple' | null>; + expect(result.value).toEqual(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); }); diff --git a/paseri-lib/src/schemas/never.test.ts b/paseri-lib/src/schemas/never.test.ts index 588b5b6..1ce2753 100644 --- a/paseri-lib/src/schemas/never.test.ts +++ b/paseri-lib/src/schemas/never.test.ts @@ -1,45 +1,20 @@ import { expect } from '@std/expect'; +import fc from 'fast-check'; import * as p from '../index.ts'; -import type { TreeNode } from '../issue.ts'; const { test } = Deno; -test('Type', async (t) => { +test('Everything fails', () => { const schema = p.never(); - await t.step('String', () => { - const data = 'Hello, world!'; - - const result = schema.safeParse(data); - if (!result.ok) { - const expectedResult: TreeNode = { type: 'leaf', code: 'invalid_type' }; - expect(result.issue).toEqual(expectedResult); - } else { - expect(result.ok).toBeFalsy(); - } - }); - - await t.step('Undefined', () => { - const data = undefined; - - const result = schema.safeParse(data); - if (!result.ok) { - const expectedResult: TreeNode = { type: 'leaf', code: 'invalid_type' }; - expect(result.issue).toEqual(expectedResult); - } else { - expect(result.ok).toBeFalsy(); - } - }); - - await t.step('Null', () => { - const data = null; - - const result = schema.safeParse(data); - if (!result.ok) { - const expectedResult: TreeNode = { type: 'leaf', code: 'invalid_type' }; - expect(result.issue).toEqual(expectedResult); - } else { - expect(result.ok).toBeFalsy(); - } - }); + fc.assert( + fc.property(fc.anything(), (data) => { + const result = schema.safeParse(data); + if (!result.ok) { + expect(result.issue).toEqual({ type: 'leaf', code: 'invalid_type' }); + } else { + expect(result.ok).toBeFalsy(); + } + }), + ); }); diff --git a/paseri-lib/src/schemas/null.test.ts b/paseri-lib/src/schemas/null.test.ts index 455f105..106e1c6 100644 --- a/paseri-lib/src/schemas/null.test.ts +++ b/paseri-lib/src/schemas/null.test.ts @@ -1,52 +1,67 @@ import { expect } from '@std/expect'; import { expectTypeOf } from 'expect-type'; +import fc from 'fast-check'; import * as p from '../index.ts'; -import type { TreeNode } from '../issue.ts'; const { test } = Deno; -test('Type', async (t) => { +test('Valid value', () => { + const schema = p.null(); + const result = schema.safeParse(null); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf; + expect(result.value).toBe(null); + } else { + expect(result.ok).toBeTruthy(); + } +}); + +test('Invalid value', () => { const schema = p.null(); - await t.step('Valid', () => { - const result = schema.safeParse(null); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf; - expect(result.value).toBe(null); - } else { - expect(result.ok).toBeTruthy(); - } - }); - - await t.step('Not null', () => { - const result = schema.safeParse(undefined); - if (!result.ok) { - const expectedResult: TreeNode = { type: 'leaf', code: 'invalid_value' }; - expect(result.issue).toEqual(expectedResult); - } else { - expect(result.ok).toBeFalsy(); - } - }); + fc.assert( + fc.property( + fc.anything().filter((value) => value !== null), + (data) => { + const result = schema.safeParse(data); + if (!result.ok) { + expect(result.issue).toEqual({ type: 'leaf', code: 'invalid_value' }); + } else { + expect(result.ok).toBeFalsy(); + } + }, + ), + ); }); test('Optional', () => { const schema = p.null().optional(); - const result = schema.safeParse(undefined); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf; - expect(result.value).toBe(undefined); - } else { - expect(result.ok).toBeTruthy(); - } + + fc.assert( + fc.property(fc.option(fc.constant(null), { nil: undefined }), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf; + expect(result.value).toEqual(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); }); test('Nullable', () => { const schema = p.null().nullable(); - const result = schema.safeParse(null); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf; - expect(result.value).toBe(null); - } else { - expect(result.ok).toBeTruthy(); - } + + fc.assert( + fc.property(fc.option(fc.constant(null), { nil: null }), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf; + expect(result.value).toEqual(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); }); diff --git a/paseri-lib/src/schemas/number.test.ts b/paseri-lib/src/schemas/number.test.ts index 7397ee9..b2f673d 100644 --- a/paseri-lib/src/schemas/number.test.ts +++ b/paseri-lib/src/schemas/number.test.ts @@ -1,213 +1,301 @@ 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('Type', async (t) => { +test('Valid type', () => { const schema = p.number(); - await t.step('Valid', () => { - const result = schema.safeParse(123); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf; - expect(result.value).toBe(123); - } else { - expect(result.ok).toBeTruthy(); - } - }); + fc.assert( + fc.property(fc.float(), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf; + expect(result.value).toBe(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); +}); - await t.step('Not a number', () => { - const result = schema.safeParse(null); - if (!result.ok) { - expect(result.issue).toEqual({ type: 'leaf', code: 'invalid_type' }); - } else { - expect(result.ok).toBeFalsy(); - } - }); +test('Invalid type', () => { + const schema = p.number(); + + fc.assert( + fc.property( + fc.anything().filter((value) => typeof value !== 'number'), + (data) => { + const result = schema.safeParse(data); + if (!result.ok) { + expect(result.issue).toEqual({ type: 'leaf', code: 'invalid_type' }); + } else { + expect(result.ok).toBeFalsy(); + } + }, + ), + ); }); -test('Greater than or equal', async (t) => { +test('Valid gte', () => { const schema = p.number().gte(10); - await t.step('Valid', () => { - const result = schema.safeParse(10); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf; - expect(result.value).toBe(10); - } else { - expect(result.ok).toBeTruthy(); - } - }); + fc.assert( + fc.property(fc.float({ noNaN: true, min: 10 }), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf; + expect(result.value).toBe(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); +}); - await t.step('Too small', () => { - const result = schema.safeParse(9); - if (!result.ok) { - expect(result.issue).toEqual({ type: 'leaf', code: 'too_small' }); - } else { - expect(result.ok).toBeFalsy(); - } - }); +test('Invalid gte', () => { + const schema = p.number().gte(10); + + fc.assert( + fc.property(fc.float({ noNaN: true, max: 10, maxExcluded: true }), (data) => { + const result = schema.safeParse(data); + if (!result.ok) { + expect(result.issue).toEqual({ type: 'leaf', code: 'too_small' }); + } else { + expect(result.ok).toBeFalsy(); + } + }), + ); }); -test('Greater than', async (t) => { +test('Valid gt', () => { const schema = p.number().gt(10); - await t.step('Valid', () => { - const result = schema.safeParse(11); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf; - expect(result.value).toBe(11); - } else { - expect(result.ok).toBeTruthy(); - } - }); + fc.assert( + fc.property(fc.float({ noNaN: true, min: 10, minExcluded: true }), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf; + expect(result.value).toBe(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); +}); - await t.step('Too small', () => { - const result = schema.safeParse(10); - if (!result.ok) { - expect(result.issue).toEqual({ type: 'leaf', code: 'too_small' }); - } else { - expect(result.ok).toBeFalsy(); - } - }); +test('Invalid gt', () => { + const schema = p.number().gt(10); + + fc.assert( + fc.property(fc.float({ noNaN: true, max: 10 }), (data) => { + const result = schema.safeParse(data); + if (!result.ok) { + expect(result.issue).toEqual({ type: 'leaf', code: 'too_small' }); + } else { + expect(result.ok).toBeFalsy(); + } + }), + ); }); -test('Less than or equal', async (t) => { +test('Valid lte', () => { const schema = p.number().lte(10); - await t.step('Valid', () => { - const result = schema.safeParse(10); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf; - expect(result.value).toBe(10); - } else { - expect(result.ok).toBeTruthy(); - } - }); + fc.assert( + fc.property(fc.float({ noNaN: true, max: 10 }), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf; + expect(result.value).toBe(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); +}); - await t.step('Too large', () => { - const result = schema.safeParse(11); - if (!result.ok) { - expect(result.issue).toEqual({ type: 'leaf', code: 'too_large' }); - } else { - expect(result.ok).toBeFalsy(); - } - }); +test('Invalid lte', () => { + const schema = p.number().lte(10); + + fc.assert( + fc.property(fc.float({ noNaN: true, min: 10, minExcluded: true }), (data) => { + const result = schema.safeParse(data); + if (!result.ok) { + expect(result.issue).toEqual({ type: 'leaf', code: 'too_large' }); + } else { + expect(result.ok).toBeFalsy(); + } + }), + ); }); -test('Less than', async (t) => { +test('Valid lt', () => { const schema = p.number().lt(10); - await t.step('Valid', () => { - const result = schema.safeParse(9); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf; - expect(result.value).toBe(9); - } else { - expect(result.ok).toBeTruthy(); - } - }); + fc.assert( + fc.property(fc.float({ noNaN: true, max: 10, maxExcluded: true }), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf; + expect(result.value).toBe(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); +}); - await t.step('Too large', () => { - const result = schema.safeParse(10); - if (!result.ok) { - expect(result.issue).toEqual({ type: 'leaf', code: 'too_large' }); - } else { - expect(result.ok).toBeFalsy(); - } - }); +test('Invalid lt', () => { + const schema = p.number().lt(10); + + fc.assert( + fc.property(fc.float({ noNaN: true, min: 10 }), (data) => { + const result = schema.safeParse(data); + if (!result.ok) { + expect(result.issue).toEqual({ type: 'leaf', code: 'too_large' }); + } else { + expect(result.ok).toBeFalsy(); + } + }), + ); }); -test('Integer', async (t) => { +test('Valid int', () => { const schema = p.number().int(); - await t.step('Valid', () => { - const result = schema.safeParse(123); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf; - expect(result.value).toBe(123); - } else { - expect(result.ok).toBeTruthy(); - } - }); + fc.assert( + fc.property(fc.integer(), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf; + expect(result.value).toBe(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); +}); - await t.step('Invalid', () => { - const result = schema.safeParse(123.4); - if (!result.ok) { - expect(result.issue).toEqual({ type: 'leaf', code: 'invalid_integer' }); - } else { - expect(result.ok).toBeFalsy(); - } - }); +test('Invalid int', () => { + const schema = p.number().int(); + + fc.assert( + fc.property(fc.float({ noInteger: true }), (data) => { + const result = schema.safeParse(data); + if (!result.ok) { + expect(result.issue).toEqual({ type: 'leaf', code: 'invalid_integer' }); + } else { + expect(result.ok).toBeFalsy(); + } + }), + ); }); -test('Finite', async (t) => { +test('Valid finite', () => { const schema = p.number().finite(); - await t.step('Valid', () => { - const result = schema.safeParse(123); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf; - expect(result.value).toBe(123); - } else { - expect(result.ok).toBeTruthy(); - } - }); + fc.assert( + fc.property(fc.float({ noNaN: true, noDefaultInfinity: true }), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf; + expect(result.value).toBe(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); +}); - await t.step('Invalid', () => { - const result = schema.safeParse(Number.NEGATIVE_INFINITY); - if (!result.ok) { - expect(result.issue).toEqual({ type: 'leaf', code: 'invalid_finite' }); - } else { - expect(result.ok).toBeFalsy(); - } - }); +test('Invalid finite', () => { + const schema = p.number().finite(); + + fc.assert( + fc.property( + fc.oneof( + fc.constant(Number.POSITIVE_INFINITY), + fc.constant(Number.NEGATIVE_INFINITY), + fc.constant(Number.NaN), + ), + (data) => { + const result = schema.safeParse(data); + if (!result.ok) { + expect(result.issue).toEqual({ type: 'leaf', code: 'invalid_finite' }); + } else { + expect(result.ok).toBeFalsy(); + } + }, + ), + ); }); -test('Safe integer', async (t) => { +test('Valid safe', () => { const schema = p.number().safe(); - await t.step('Valid', () => { - const result = schema.safeParse(123); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf; - expect(result.value).toBe(123); - } else { - expect(result.ok).toBeTruthy(); - } - }); + fc.assert( + fc.property(fc.maxSafeInteger(), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf; + expect(result.value).toBe(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); +}); - await t.step('Invalid', () => { - const result = schema.safeParse(Number.MAX_SAFE_INTEGER + 1); - if (!result.ok) { - expect(result.issue).toEqual({ type: 'leaf', code: 'invalid_safe_integer' }); - } else { - expect(result.ok).toBeFalsy(); - } - }); +test('Invalid safe', () => { + const schema = p.number().safe(); + + fc.assert( + fc.property( + fc.oneof(fc.constant(Number.MAX_SAFE_INTEGER + 1), fc.constant(Number.MIN_SAFE_INTEGER - 1)), + (data) => { + const result = schema.safeParse(data); + if (!result.ok) { + expect(result.issue).toEqual({ type: 'leaf', code: 'invalid_safe_integer' }); + } else { + expect(result.ok).toBeFalsy(); + } + }, + ), + ); }); test('Optional', () => { const schema = p.number().optional(); - const result = schema.safeParse(undefined); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf; - expect(result.value).toBe(undefined); - } else { - expect(result.ok).toBeTruthy(); - } + + fc.assert( + fc.property(fc.option(fc.float(), { nil: undefined }), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf; + expect(result.value).toEqual(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); }); test('Nullable', () => { const schema = p.number().nullable(); - const result = schema.safeParse(null); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf; - expect(result.value).toBe(null); - } else { - expect(result.ok).toBeTruthy(); - } + + fc.assert( + fc.property(fc.option(fc.float(), { nil: null }), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf; + expect(result.value).toEqual(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); }); test('Immutable', async (t) => { diff --git a/paseri-lib/src/schemas/object.test.ts b/paseri-lib/src/schemas/object.test.ts index 37a586d..da274f4 100644 --- a/paseri-lib/src/schemas/object.test.ts +++ b/paseri-lib/src/schemas/object.test.ts @@ -1,151 +1,144 @@ import { expect } from '@std/expect'; import { expectTypeOf } from 'expect-type'; +import fc from 'fast-check'; import * as p from '../index.ts'; import type { TreeNode } from '../issue.ts'; const { test } = Deno; -test('Type', async (t) => { - const schema = p.object({ - child: p.string(), - }); - - await t.step('Valid', () => { - const data = Object.freeze({ child: 'hello' }); - - const result = schema.safeParse(data); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf<{ child: string }>; - const expectedResult = { child: 'hello' }; - expect(result.value).toEqual(expectedResult); - } else { - expect(result.ok).toBeTruthy(); - } - }); - - await t.step('Not an object', () => { - const data = null; - - const result = schema.safeParse(data); - if (!result.ok) { - const expectedResult: TreeNode = { type: 'leaf', code: 'invalid_type' }; - expect(result.issue).toEqual(expectedResult); - } else { - expect(result.ok).toBeFalsy(); - } - }); +test('Valid type', () => { + const schema = p.object({ foo: p.string() }); + + fc.assert( + fc.property(fc.record({ foo: fc.string() }), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf<{ foo: string }>; + expect(result.value).toEqual(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); }); -test('Flat, strip', () => { - const schema = p.object({ - child: p.string(), - }); - const data = Object.freeze({ extra1: 'foo', child: 'hello', extra2: 'bar' }); - - const result = schema.safeParse(data); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf<{ child: string }>; - const expectedResult = { child: 'hello' }; - expect(result.value).toEqual(expectedResult); - } else { - expect(result.ok).toBeTruthy(); - } +test('Invalid type', () => { + const schema = p.object({ foo: p.string() }); + + fc.assert( + fc.property( + fc.anything().filter((value) => !(typeof value === 'object' && value !== null)), + (data) => { + const result = schema.safeParse(data); + if (!result.ok) { + expect(result.issue).toEqual({ type: 'leaf', code: 'invalid_type' }); + } else { + expect(result.ok).toBeFalsy(); + } + }, + ), + ); }); -test('Deep, strip', () => { +test('Strip', () => { const schema = p.object({ - child: p.object({ - expected: p.string(), + foo: p.string(), + bar: p.object({ + baz: p.number(), }), }); - const data = Object.freeze({ child: { extra1: 'foo', expected: 'hello', extra2: 'bar' } }); - const result = schema.safeParse(data); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf<{ child: { expected: string } }>; - const expectedResult = { child: { expected: 'hello' } }; - expect(result.value).toEqual(expectedResult); - } else { - expect(result.ok).toBeTruthy(); - } + fc.assert( + fc.property( + fc.record({ + foo: fc.string(), + bar: fc.record({ baz: fc.float(), extra2: fc.anything() }), + extra1: fc.anything(), + }), + (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf<{ foo: string; bar: { baz: number } }>; + const expectedResult = { foo: data.foo, bar: { baz: data.bar.baz } }; + expect(result.value).toEqual(expectedResult); + } else { + expect(result.ok).toBeTruthy(); + } + }, + ), + ); }); -test('Flat, strict', () => { +test('Strict', () => { const schema = p .object({ - child: p.string(), + foo: p.string(), + bar: p + .object({ + baz: p.number(), + }) + .strict(), }) .strict(); - const data = Object.freeze({ extra1: 'foo', child: 'hello', extra2: 'bar' }); - - const result = schema.safeParse(data); - if (!result.ok) { - const expectedResult: TreeNode = { - type: 'join', - left: { type: 'nest', key: 'extra1', child: { type: 'leaf', code: 'unrecognized_key' } }, - right: { type: 'nest', key: 'extra2', child: { type: 'leaf', code: 'unrecognized_key' } }, - }; - expect(result.issue).toEqual(expectedResult); - } else { - expect(result.ok).toBeFalsy(); - } -}); - -test('Deep, strict', () => { - const schema = p.object({ - child: p.object({ expected: p.string() }).strict(), - }); - const data = Object.freeze({ child: { extra1: 'foo', expected: 'hello', extra2: 'bar' } }); - const result = schema.safeParse(data); - if (!result.ok) { - const expectedResult: TreeNode = { - type: 'nest', - key: 'child', - child: { - type: 'join', - left: { type: 'nest', key: 'extra1', child: { type: 'leaf', code: 'unrecognized_key' } }, - right: { type: 'nest', key: 'extra2', child: { type: 'leaf', code: 'unrecognized_key' } }, + fc.assert( + fc.property( + fc.record({ + foo: fc.string(), + bar: fc.record({ baz: fc.float(), extra2: fc.anything() }), + extra1: fc.anything(), + }), + (data) => { + const result = schema.safeParse(data); + if (!result.ok) { + const expectedResult: TreeNode = { + type: 'join', + left: { + type: 'nest', + key: 'bar', + child: { type: 'nest', key: 'extra2', child: { type: 'leaf', code: 'unrecognized_key' } }, + }, + right: { type: 'nest', key: 'extra1', child: { type: 'leaf', code: 'unrecognized_key' } }, + }; + expect(result.issue).toEqual(expectedResult); + } else { + expect(result.ok).toBeFalsy(); + } }, - }; - expect(result.issue).toEqual(expectedResult); - } else { - expect(result.ok).toBeFalsy(); - } + ), + ); }); -test('Flat, passthrough', () => { +test('Passthrough', () => { const schema = p .object({ - child: p.string(), + foo: p.string(), + bar: p + .object({ + baz: p.number(), + }) + .passthrough(), }) .passthrough(); - const data = Object.freeze({ extra1: 'foo', child: 'hello', extra2: 'bar' }); - - const result = schema.safeParse(data); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf<{ child: string }>; - const expectedResult = { extra1: 'foo', child: 'hello', extra2: 'bar' }; - expect(result.value).toEqual(expectedResult); - } else { - expect(result.ok).toBeTruthy(); - } -}); - -test('Deep, passthrough', () => { - const schema = p.object({ - child: p.object({ expected: p.string() }).passthrough(), - }); - const data = Object.freeze({ child: { extra1: 'foo', expected: 'hello', extra2: 'bar' } }); - const result = schema.safeParse(data); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf<{ child: { expected: string } }>; - const expectedResult = { child: { extra1: 'foo', expected: 'hello', extra2: 'bar' } }; - expect(result.value).toEqual(expectedResult); - } else { - expect(result.ok).toBeTruthy(); - } + fc.assert( + fc.property( + fc.record({ + foo: fc.string(), + bar: fc.record({ baz: fc.float(), extra2: fc.anything() }), + extra1: fc.anything(), + }), + (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf<{ foo: string; bar: { baz: number } }>; + expect(result.value).toEqual(data); + } else { + expect(result.ok).toBeTruthy(); + } + }, + ), + ); }); test('Missing keys', () => { @@ -243,55 +236,73 @@ test('Optional key is not flagged as missing', () => { }); test('Optional', () => { - const schema = p.object({ child: p.string() }).optional(); - const data = undefined; - - const result = schema.safeParse(data); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf<{ child: string } | undefined>; - expect(result.value).toBe(undefined); - } else { - expect(result.ok).toBeTruthy(); - } -}); - -test('Deep optional', () => { - const schema = p.object({ child: p.string().optional() }); - const data = Object.freeze({ child: undefined }); - - const result = schema.safeParse(data); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf<{ child: string | undefined }>; - expect(result.value).toEqual({ child: undefined }); - } else { - expect(result.ok).toBeTruthy(); - } + const schema = p + .object({ + foo: p.string(), + bar: p + .object({ + baz: p.number(), + }) + .optional(), + }) + .optional(); + + fc.assert( + fc.property( + fc.option( + fc.record({ + foo: fc.string(), + bar: fc.option(fc.record({ baz: fc.float() }), { nil: undefined }), + }), + { nil: undefined }, + ), + (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf< + { foo: string; bar: { baz: number } | undefined } | undefined + >; + expect(result.value).toEqual(data); + } else { + expect(result.ok).toBeTruthy(); + } + }, + ), + ); }); test('Nullable', () => { - const schema = p.object({ child: p.string() }).nullable(); - const data = null; - - const result = schema.safeParse(data); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf<{ child: string } | null>; - expect(result.value).toBe(null); - } else { - expect(result.ok).toBeTruthy(); - } -}); - -test('Deep nullable', () => { - const schema = p.object({ child: p.string().nullable() }); - const data = Object.freeze({ child: null }); - - const result = schema.safeParse(data); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf<{ child: string | null }>; - expect(result.value).toEqual({ child: null }); - } else { - expect(result.ok).toBeTruthy(); - } + const schema = p + .object({ + foo: p.string(), + bar: p + .object({ + baz: p.number(), + }) + .nullable(), + }) + .nullable(); + + fc.assert( + fc.property( + fc.option( + fc.record({ + foo: fc.string(), + bar: fc.option(fc.record({ baz: fc.float() }), { nil: null }), + }), + { nil: null }, + ), + (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf<{ foo: string; bar: { baz: number } | null } | null>; + expect(result.value).toEqual(data); + } else { + expect(result.ok).toBeTruthy(); + } + }, + ), + ); }); test('White-box', async (t) => { diff --git a/paseri-lib/src/schemas/record.test.ts b/paseri-lib/src/schemas/record.test.ts index 3d832f7..1766bc6 100644 --- a/paseri-lib/src/schemas/record.test.ts +++ b/paseri-lib/src/schemas/record.test.ts @@ -1,32 +1,43 @@ import { expect } from '@std/expect'; import { expectTypeOf } from 'expect-type'; +import fc from 'fast-check'; import * as p from '../index.ts'; import type { TreeNode } from '../issue.ts'; const { test } = Deno; -test('Type', async (t) => { +test('Valid type', () => { const schema = p.record(p.number()); - await t.step('Valid', () => { - const result = schema.safeParse({ foo: 123, bar: 456 }); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf>; - expect(result.value).toEqual({ foo: 123, bar: 456 }); - } else { - expect(result.ok).toBeTruthy(); - } - }); + fc.assert( + fc.property(fc.object({ values: [fc.float()], maxDepth: 0 }), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf>; + expect(result.value).toBe(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); +}); + +test('Invalid type', () => { + const schema = p.record(p.number()); - await t.step('Invalid', () => { - const result = schema.safeParse(null); - if (!result.ok) { - const expectedResult: TreeNode = { type: 'leaf', code: 'invalid_type' }; - expect(result.issue).toEqual(expectedResult); - } else { - expect(result.ok).toBeFalsy(); - } - }); + fc.assert( + fc.property( + fc.anything().filter((value) => !(typeof value === 'object' && value !== null)), + (data) => { + const result = schema.safeParse(data); + if (!result.ok) { + expect(result.issue).toEqual({ type: 'leaf', code: 'invalid_type' }); + } else { + expect(result.ok).toBeFalsy(); + } + }, + ), + ); }); test('Invalid elements', () => { @@ -48,22 +59,32 @@ test('Invalid elements', () => { test('Optional', () => { const schema = p.record(p.number()).optional(); - const result = schema.safeParse(undefined); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf | undefined>; - expect(result.value).toBe(undefined); - } else { - expect(result.ok).toBeTruthy(); - } + + fc.assert( + fc.property(fc.option(fc.object({ values: [fc.float()], maxDepth: 0 }), { nil: undefined }), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf | undefined>; + expect(result.value).toEqual(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); }); test('Nullable', () => { const schema = p.record(p.number()).nullable(); - const result = schema.safeParse(null); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf | null>; - expect(result.value).toBe(null); - } else { - expect(result.ok).toBeTruthy(); - } + + fc.assert( + fc.property(fc.option(fc.object({ values: [fc.float()], maxDepth: 0 }), { nil: null }), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf | null>; + expect(result.value).toEqual(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); }); diff --git a/paseri-lib/src/schemas/string.test.ts b/paseri-lib/src/schemas/string.test.ts index c178535..84935f4 100644 --- a/paseri-lib/src/schemas/string.test.ts +++ b/paseri-lib/src/schemas/string.test.ts @@ -1,111 +1,155 @@ import { expect } from '@std/expect'; import { expectTypeOf } from 'expect-type'; +import fc from 'fast-check'; import * as p from '../index.ts'; +import { nanoidRegex } from './string.ts'; const { test } = Deno; -test('Type', async (t) => { +test('Valid type', () => { const schema = p.string(); - await t.step('Valid', () => { - const result = schema.safeParse('Hello, world!'); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf; - expect(result.value).toBe('Hello, world!'); - } else { - expect(result.ok).toBeTruthy(); - } - }); + fc.assert( + fc.property(fc.string(), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf; + expect(result.value).toBe(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); +}); - await t.step('Not a string', () => { - const result = schema.safeParse(null); - if (!result.ok) { - expect(result.issue).toEqual({ type: 'leaf', code: 'invalid_type' }); - } else { - expect(result.ok).toBeFalsy(); - } - }); +test('Invalid type', () => { + const schema = p.string(); + + fc.assert( + fc.property( + fc.anything().filter((value) => typeof value !== 'string'), + (data) => { + const result = schema.safeParse(data); + if (!result.ok) { + expect(result.issue).toEqual({ type: 'leaf', code: 'invalid_type' }); + } else { + expect(result.ok).toBeFalsy(); + } + }, + ), + ); }); -test('Min', async (t) => { +test('Valid min', () => { const schema = p.string().min(3); - await t.step('Valid', () => { - const result = schema.safeParse('aaa'); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf; - expect(result.value).toBe('aaa'); - } else { - expect(result.ok).toBeTruthy(); - } - }); + fc.assert( + fc.property(fc.string({ minLength: 3 }), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf; + expect(result.value).toBe(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); +}); - await t.step('Too short', () => { - const result = schema.safeParse('aa'); - if (!result.ok) { - expect(result.issue).toEqual({ type: 'leaf', code: 'too_short' }); - } else { - expect(result.ok).toBeFalsy(); - } - }); +test('Invalid min', () => { + const schema = p.string().min(3); + + fc.assert( + fc.property(fc.string({ maxLength: 2 }), (data) => { + const result = schema.safeParse(data); + if (!result.ok) { + expect(result.issue).toEqual({ type: 'leaf', code: 'too_short' }); + } else { + expect(result.ok).toBeFalsy(); + } + }), + ); }); -test('Max', async (t) => { +test('Valid max', () => { const schema = p.string().max(3); - await t.step('Valid', () => { - const result = schema.safeParse('aaa'); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf; - expect(result.value).toBe('aaa'); - } else { - expect(result.ok).toBeTruthy(); - } - }); + fc.assert( + fc.property(fc.string({ maxLength: 3 }), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf; + expect(result.value).toBe(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); +}); - await t.step('Too long', () => { - const result = schema.safeParse('aaaa'); - if (!result.ok) { - expect(result.issue).toEqual({ type: 'leaf', code: 'too_long' }); - } else { - expect(result.ok).toBeFalsy(); - } - }); +test('Invalid max', () => { + const schema = p.string().max(3); + + fc.assert( + fc.property(fc.string({ minLength: 4 }), (data) => { + const result = schema.safeParse(data); + if (!result.ok) { + expect(result.issue).toEqual({ type: 'leaf', code: 'too_long' }); + } else { + expect(result.ok).toBeFalsy(); + } + }), + ); }); -test('Length', async (t) => { +test('Valid length', () => { const schema = p.string().length(3); - await t.step('Valid', () => { - const result = schema.safeParse('aaa'); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf; - expect(result.value).toBe('aaa'); - } else { - expect(result.ok).toBeTruthy(); - } - }); + fc.assert( + fc.property(fc.string({ minLength: 3, maxLength: 3 }), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf; + expect(result.value).toBe(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); +}); - await t.step('Too long', () => { - const result = schema.safeParse('aaaa'); - if (!result.ok) { - expect(result.issue).toEqual({ type: 'leaf', code: 'too_long' }); - } else { - expect(result.ok).toBeFalsy(); - } - }); +test('Invalid length (too long)', () => { + const schema = p.string().length(3); - await t.step('Too short', () => { - const result = schema.safeParse('aa'); - if (!result.ok) { - expect(result.issue).toEqual({ type: 'leaf', code: 'too_short' }); - } else { - expect(result.ok).toBeFalsy(); - } - }); + fc.assert( + fc.property(fc.string({ minLength: 4 }), (data) => { + const result = schema.safeParse(data); + if (!result.ok) { + expect(result.issue).toEqual({ type: 'leaf', code: 'too_long' }); + } else { + expect(result.ok).toBeFalsy(); + } + }), + ); +}); + +test('Invalid length (too short)', () => { + const schema = p.string().length(3); + + fc.assert( + fc.property(fc.string({ maxLength: 2 }), (data) => { + const result = schema.safeParse(data); + if (!result.ok) { + expect(result.issue).toEqual({ type: 'leaf', code: 'too_short' }); + } else { + expect(result.ok).toBeFalsy(); + } + }), + ); }); test('Email', async (t) => { + // TODO: Use fast-check once it has better support for the email regex. const schema = p.string().email(); await t.step('Valid', () => { @@ -129,6 +173,7 @@ test('Email', async (t) => { }); test('Emoji', async (t) => { + // TODO: Use fast-check once it has better support for the emoji regex. const schema = p.string().emoji(); await t.step('Valid', () => { @@ -151,72 +196,102 @@ test('Emoji', async (t) => { }); }); -test('UUID', async (t) => { +test('Valid uuid', () => { const schema = p.string().uuid(); - await t.step('Valid', () => { - const result = schema.safeParse('d98d4b7e-58a5-4e21-839b-2699b94c115b'); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf; - expect(result.value).toBe('d98d4b7e-58a5-4e21-839b-2699b94c115b'); - } else { - expect(result.ok).toBeTruthy(); - } - }); + fc.assert( + fc.property(fc.uuid(), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf; + expect(result.value).toBe(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); +}); - await t.step('Invalid', () => { - const result = schema.safeParse('not_a_uuid'); - if (!result.ok) { - expect(result.issue).toEqual({ type: 'leaf', code: 'invalid_uuid' }); - } else { - expect(result.ok).toBeFalsy(); - } - }); +test('Invalid uuid', () => { + const schema = p.string().uuid(); + + fc.assert( + // It's possible that this will generate a valid UUID, but *extremely* unlikely. + fc.property(fc.string(), (data) => { + const result = schema.safeParse(data); + if (!result.ok) { + expect(result.issue).toEqual({ type: 'leaf', code: 'invalid_uuid' }); + } else { + expect(result.ok).toBeFalsy(); + } + }), + ); }); -test('Nano ID', async (t) => { +test('Valid Nano ID', () => { const schema = p.string().nanoid(); + // FIXME: fast-check doesn't like case-insensitive regexes. + const regex = new RegExp(nanoidRegex.source); + + fc.assert( + fc.property(fc.stringMatching(regex), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf; + expect(result.value).toBe(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); +}); - await t.step('Valid', () => { - const result = schema.safeParse('V1StGXR8_Z5jdHi6B-myT'); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf; - expect(result.value).toBe('V1StGXR8_Z5jdHi6B-myT'); - } else { - expect(result.ok).toBeTruthy(); - } - }); +test('Invalid Nano ID', () => { + const schema = p.string().nanoid(); - await t.step('Invalid', () => { - const result = schema.safeParse('not_a_nano_id'); - if (!result.ok) { - expect(result.issue).toEqual({ type: 'leaf', code: 'invalid_nanoid' }); - } else { - expect(result.ok).toBeFalsy(); - } - }); + fc.assert( + // It's possible that this will generate a valid Nano, but *extremely* unlikely. + fc.property(fc.string(), (data) => { + const result = schema.safeParse(data); + if (!result.ok) { + expect(result.issue).toEqual({ type: 'leaf', code: 'invalid_nanoid' }); + } else { + expect(result.ok).toBeFalsy(); + } + }), + ); }); test('Optional', () => { const schema = p.string().optional(); - const result = schema.safeParse(undefined); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf; - expect(result.value).toBe(undefined); - } else { - expect(result.ok).toBeTruthy(); - } + + fc.assert( + fc.property(fc.option(fc.string(), { nil: undefined }), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf; + expect(result.value).toEqual(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); }); test('Nullable', () => { const schema = p.string().nullable(); - const result = schema.safeParse(null); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf; - expect(result.value).toBe(null); - } else { - expect(result.ok).toBeTruthy(); - } + + fc.assert( + fc.property(fc.option(fc.string(), { nil: null }), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf; + expect(result.value).toEqual(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); }); test('Immutable', async (t) => { diff --git a/paseri-lib/src/schemas/string.ts b/paseri-lib/src/schemas/string.ts index c51c5aa..1cfe817 100644 --- a/paseri-lib/src/schemas/string.ts +++ b/paseri-lib/src/schemas/string.ts @@ -133,4 +133,4 @@ function string() { return singleton; } -export { string }; +export { string, emailRegex, emojiRegex, uuidRegex, nanoidRegex }; diff --git a/paseri-lib/src/schemas/tuple.test.ts b/paseri-lib/src/schemas/tuple.test.ts index 63ac881..7e7c296 100644 --- a/paseri-lib/src/schemas/tuple.test.ts +++ b/paseri-lib/src/schemas/tuple.test.ts @@ -1,32 +1,43 @@ import { expect } from '@std/expect'; import { expectTypeOf } from 'expect-type'; +import fc from 'fast-check'; import * as p from '../index.ts'; import type { TreeNode } from '../issue.ts'; const { test } = Deno; -test('Type', async (t) => { +test('Valid type', () => { const schema = p.tuple(p.number(), p.string(), p.literal(123n)); - await t.step('Valid', () => { - const result = schema.safeParse([1, 'foo', 123n]); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf<[number, string, 123n]>; - expect(result.value).toEqual([1, 'foo', 123n]); - } else { - expect(result.ok).toBeTruthy(); - } - }); + fc.assert( + fc.property(fc.tuple(fc.float(), fc.string(), fc.constant(123n)), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf<[number, string, 123n]>; + expect(result.value).toBe(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); +}); + +test('Invalid type', () => { + const schema = p.tuple(p.number(), p.string(), p.literal(123n)); - await t.step('Invalid', () => { - const result = schema.safeParse(null); - if (!result.ok) { - const expectedResult: TreeNode = { type: 'leaf', code: 'invalid_type' }; - expect(result.issue).toEqual(expectedResult); - } else { - expect(result.ok).toBeFalsy(); - } - }); + fc.assert( + fc.property( + fc.anything().filter((value) => !Array.isArray(value)), + (data) => { + const result = schema.safeParse(data); + if (!result.ok) { + expect(result.issue).toEqual({ type: 'leaf', code: 'invalid_type' }); + } else { + expect(result.ok).toBeFalsy(); + } + }, + ), + ); }); test('Too long', () => { @@ -80,22 +91,32 @@ test('Invalid elements', () => { test('Optional', () => { const schema = p.tuple(p.number(), p.string(), p.literal(123n)).optional(); - const result = schema.safeParse(undefined); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf<[number, string, 123n] | undefined>; - expect(result.value).toBe(undefined); - } else { - expect(result.ok).toBeTruthy(); - } + + fc.assert( + fc.property(fc.option(fc.tuple(fc.float(), fc.string(), fc.constant(123n)), { nil: undefined }), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf<[number, string, 123n] | undefined>; + expect(result.value).toEqual(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); }); test('Nullable', () => { const schema = p.tuple(p.number(), p.string(), p.literal(123n)).nullable(); - const result = schema.safeParse(null); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf<[number, string, 123n] | null>; - expect(result.value).toBe(null); - } else { - expect(result.ok).toBeTruthy(); - } + + fc.assert( + fc.property(fc.option(fc.tuple(fc.float(), fc.string(), fc.constant(123n)), { nil: null }), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf<[number, string, 123n] | null>; + expect(result.value).toEqual(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); }); diff --git a/paseri-lib/src/schemas/undefined.test.ts b/paseri-lib/src/schemas/undefined.test.ts index 42fa35c..0def079 100644 --- a/paseri-lib/src/schemas/undefined.test.ts +++ b/paseri-lib/src/schemas/undefined.test.ts @@ -1,41 +1,67 @@ import { expect } from '@std/expect'; import { expectTypeOf } from 'expect-type'; +import fc from 'fast-check'; import * as p from '../index.ts'; -import type { TreeNode } from '../issue.ts'; const { test } = Deno; -test('Type', async (t) => { +test('Valid value', () => { + const schema = p.undefined(); + const result = schema.safeParse(undefined); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf; + expect(result.value).toBe(undefined); + } else { + expect(result.ok).toBeTruthy(); + } +}); + +test('Invalid value', () => { const schema = p.undefined(); - await t.step('Valid', () => { - const result = schema.safeParse(undefined); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf; - expect(result.value).toBe(undefined); - } else { - expect(result.ok).toBeTruthy(); - } - }); - - await t.step('Not null', () => { - const result = schema.safeParse(null); - if (!result.ok) { - const expectedResult: TreeNode = { type: 'leaf', code: 'invalid_value' }; - expect(result.issue).toEqual(expectedResult); - } else { - expect(result.ok).toBeFalsy(); - } - }); + fc.assert( + fc.property( + fc.anything().filter((value) => value !== undefined), + (data) => { + const result = schema.safeParse(data); + if (!result.ok) { + expect(result.issue).toEqual({ type: 'leaf', code: 'invalid_value' }); + } else { + expect(result.ok).toBeFalsy(); + } + }, + ), + ); +}); + +test('Optional', () => { + const schema = p.undefined().optional(); + + fc.assert( + fc.property(fc.option(fc.constant(undefined), { nil: undefined }), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf; + expect(result.value).toEqual(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); }); test('Nullable', () => { const schema = p.undefined().nullable(); - const result = schema.safeParse(null); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf; - expect(result.value).toBe(null); - } else { - expect(result.ok).toBeTruthy(); - } + + fc.assert( + fc.property(fc.option(fc.constant(undefined), { nil: null }), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf; + expect(result.value).toEqual(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); }); diff --git a/paseri-lib/src/schemas/union.test.ts b/paseri-lib/src/schemas/union.test.ts index 3bfeeb7..9e630b1 100644 --- a/paseri-lib/src/schemas/union.test.ts +++ b/paseri-lib/src/schemas/union.test.ts @@ -1,72 +1,74 @@ import { expect } from '@std/expect'; import { expectTypeOf } from 'expect-type'; +import fc from 'fast-check'; import * as p from '../index.ts'; -import type { TreeNode } from '../issue.ts'; const { test } = Deno; -test('Type', async (t) => { +test('Valid type', () => { const schema = p.union(p.string(), p.number(), p.literal(123n)); - await t.step('Valid string', () => { - const result = schema.safeParse('Hello, world!'); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf; - expect(result.value).toBe('Hello, world!'); - } else { - expect(result.ok).toBeTruthy(); - } - }); - - await t.step('Valid number', () => { - const result = schema.safeParse(1); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf; - expect(result.value).toBe(1); - } else { - expect(result.ok).toBeTruthy(); - } - }); + fc.assert( + fc.property(fc.oneof(fc.string(), fc.float(), fc.constant(123n)), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf; + expect(result.value).toEqual(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); +}); - await t.step('Valid literal', () => { - const result = schema.safeParse(123n); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf; - expect(result.value).toBe(123n); - } else { - expect(result.ok).toBeTruthy(); - } - }); +test('Invalid type', () => { + const schema = p.union(p.string(), p.number(), p.literal(123n)); - await t.step('Invalid', () => { - const result = schema.safeParse(null); - if (!result.ok) { - const expectedResult: TreeNode = { type: 'leaf', code: 'invalid_value' }; - expect(result.issue).toEqual(expectedResult); - } else { - expect(result.ok).toBeFalsy(); - } - }); + fc.assert( + fc.property( + fc + .anything() + .filter((value) => !(typeof value === 'string' || typeof value === 'number' || value === 123n)), + (data) => { + const result = schema.safeParse(data); + if (!result.ok) { + expect(result.issue).toEqual({ type: 'leaf', code: 'invalid_value' }); + } else { + expect(result.ok).toBeFalsy(); + } + }, + ), + ); }); test('Optional', () => { const schema = p.union(p.string(), p.number(), p.literal(123n)).optional(); - const result = schema.safeParse(undefined); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf; - expect(result.value).toBe(undefined); - } else { - expect(result.ok).toBeTruthy(); - } + + fc.assert( + fc.property(fc.option(fc.oneof(fc.string(), fc.float(), fc.constant(123n)), { nil: undefined }), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf; + expect(result.value).toEqual(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); }); test('Nullable', () => { const schema = p.union(p.string(), p.number(), p.literal(123n)).nullable(); - const result = schema.safeParse(null); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf; - expect(result.value).toBe(null); - } else { - expect(result.ok).toBeTruthy(); - } + + fc.assert( + fc.property(fc.option(fc.oneof(fc.string(), fc.float(), fc.constant(123n)), { nil: null }), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf; + expect(result.value).toEqual(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); }); diff --git a/paseri-lib/src/schemas/unknown.test.ts b/paseri-lib/src/schemas/unknown.test.ts index 6d88af2..416d1bc 100644 --- a/paseri-lib/src/schemas/unknown.test.ts +++ b/paseri-lib/src/schemas/unknown.test.ts @@ -1,39 +1,22 @@ 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('Type', async (t) => { +test('Everything succeeds', () => { const schema = p.unknown(); - await t.step('String', () => { - const result = schema.safeParse('Hello, world!'); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf; - expect(result.value).toBe('Hello, world!'); - } else { - expect(result.ok).toBeTruthy(); - } - }); - - await t.step('Number', () => { - const result = schema.safeParse(123); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf; - expect(result.value).toBe(123); - } else { - expect(result.ok).toBeTruthy(); - } - }); - - await t.step('Object', () => { - const result = schema.safeParse({}); - if (result.ok) { - expectTypeOf(result.value).toEqualTypeOf; - expect(result.value).toEqual({}); - } else { - expect(result.ok).toBeTruthy(); - } - }); + fc.assert( + fc.property(fc.anything(), (data) => { + const result = schema.safeParse(data); + if (result.ok) { + expectTypeOf(result.value).toEqualTypeOf; + expect(result.value).toEqual(data); + } else { + expect(result.ok).toBeTruthy(); + } + }), + ); });