diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d3af895..4604aa7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - node-version: [12.x] + node-version: [14.x] os: [ubuntu-latest, windows-latest, macOS-latest] steps: - uses: actions/checkout@v2 diff --git a/.gitignore b/.gitignore index 7fad37e..4602974 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules target index.js spec.js + diff --git a/.vscode/settings.json b/.vscode/settings.json index c72dbd0..e4c4348 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,7 @@ { "files.exclude": { "node_modules": true, - "package-lock.json": true + "package-lock.json": true, + "target": false } } \ No newline at end of file diff --git a/changelog.md b/changelog.md index d48aa55..5ebe61f 100644 --- a/changelog.md +++ b/changelog.md @@ -1,13 +1,35 @@ +## [0.18.0](https://www.npmjs.com/package/@sinclair/typebox/v/0.18.0) + +Changes: + +- Function `Type.Intersect(...)` is now implemented with `allOf` and constrained with `unevaluatedProperties` (draft `2019-09`) +- Function `Type.Dict(...)` has been deprecated and replaced with `Type.Record(...)`. +- Function `Type.Strict(...)` now includes the `$schema` property referencing the `2019-09` draft. + +### Type.Intersect(...) + +TypeBox now targets JSON schema draft `2019-09` for expressing `Type.Intersect(...)`. This is now expressed via `allOf` with additionalProperties constrained with `unevaluatedProperties`. Note that `unevaluatedProperties` is a feature of the `2019-09` specification. + +### Type.Record(K, V) + +TypeBox has deprecated `Type.Dict(...)` in favor of the more generic `Type.Record(...)`. Where as `Type.Dict(...)` was previously expressed with `additionalProperties: { ... }`, `Type.Record(...)` is expressed with `patternProperties` and supports both `string` and `number` indexer keys. Additionally, `Type.Record(...)` supports string union arguments. This is analogous to TypeScript's utility record type `Record<'a' | 'b' | 'c', T>`. + ## [0.17.7](https://www.npmjs.com/package/@sinclair/typebox/v/0.17.7) +Changes: + - Added optional `$id` argument on `Type.Rec()`. - Documentation updates. ## [0.17.6](https://www.npmjs.com/package/@sinclair/typebox/v/0.17.6) +Changes: + - Added `Type.Rec(...)` function. +Notes: + This update introduces the `Type.Rec()` function for enabling Recursive Types. Please note that due to current inference limitations in TypeScript, TypeBox is unable to infer the type and resolves inner types to `any`. This functionality enables for complex self referential schemas to be composed. The following creates a binary expression syntax node with the expression self referential for left and right oprands. @@ -59,8 +81,12 @@ This functionality is flagged as `EXPERIMENTAL` and awaits community feedback. ## [0.17.4](https://www.npmjs.com/package/@sinclair/typebox/v/0.17.4) +Changes: + - Added `Type.Box()` and `Type.Ref()` functions. +Notes: + This update provides the `Type.Box()` function to enable common related schemas to grouped under a common namespace; typically expressed as a `URI`. This functionality is primarily geared towards allowing one to define a common set of domain objects that may be shared across application domains running over a network. The `Type.Box()` is intended to be an analog to `XML` `xmlns` namespacing. The `Type.Ref()` function is limited to referencing from boxes only. The following is an example. diff --git a/example/index.ts b/example/index.ts index 4c2b8b8..d00e63b 100644 --- a/example/index.ts +++ b/example/index.ts @@ -1,10 +1,5 @@ import { Type, Static } from '@sinclair/typebox' -const Box = Type.Box('foo', { - Foo: Type.String() -}) - -const Foo = Type.Ref(Box, 'Foo') - -type Foo = Static +const Record = Type.Record(Type.String(), Type.String()) +type Record = Static diff --git a/hammer.task.ts b/hammer.task.ts index 0f97237..0c44e84 100644 --- a/hammer.task.ts +++ b/hammer.task.ts @@ -1,22 +1,18 @@ import { folder, shell } from '@sinclair/hammer' -// Cleans this projects build artifacts. export async function clean() { await folder('target').delete() } -// Runs the specs for this project. export async function spec(target = 'target/spec') { await shell(`hammer build ./spec/index.ts --dist ${target} --platform node`) await shell(`mocha ${target}/index.js`) } -// Runs the example in watch mode. export async function example(target = 'target/example') { await shell(`hammer run example/index.ts --dist ${target}`) } -// Builds this package and packs it for npm publishing. export async function build(target = 'target/build') { await folder(target).delete() await shell(`tsc -p ./src/tsconfig.json --outDir ${target}`) @@ -24,6 +20,8 @@ export async function build(target = 'target/build') { await folder(target).add('readme.md') await folder(target).add('license') await shell(`cd ${target} && npm pack`) - + // npm publish sinclair-typebox-0.x.x.tgz --access=public + // git tag + // git push --tags } diff --git a/license b/license index 4417d36..2e5d873 100644 --- a/license +++ b/license @@ -1,4 +1,4 @@ -TypeBox: JSON Schema Type Builder with Static Type Resolution for TypeScript +TypeBox: JSON Schema Type Builder with Static Type Resolution for TypeScript The MIT License (MIT) diff --git a/media/render.png b/media/render.png deleted file mode 100644 index c6c3694..0000000 Binary files a/media/render.png and /dev/null differ diff --git a/media/schema.png b/media/schema.png deleted file mode 100644 index 822bd3f..0000000 Binary files a/media/schema.png and /dev/null differ diff --git a/media/typebox.blend b/media/typebox.blend deleted file mode 100644 index 24c46f6..0000000 Binary files a/media/typebox.blend and /dev/null differ diff --git a/media/typebox.blend1 b/media/typebox.blend1 deleted file mode 100644 index 7cd1806..0000000 Binary files a/media/typebox.blend1 and /dev/null differ diff --git a/media/typebox.png b/media/typebox.png deleted file mode 100644 index 7edce19..0000000 Binary files a/media/typebox.png and /dev/null differ diff --git a/media/typescript.png b/media/typescript.png deleted file mode 100644 index a150b5d..0000000 Binary files a/media/typescript.png and /dev/null differ diff --git a/package-lock.json b/package-lock.json index 47ab9cc..c0801cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@sinclair/typebox", - "version": "0.17.1", + "version": "0.17.7", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@sinclair/typebox", - "version": "0.17.1", + "version": "0.17.7", "license": "MIT", "devDependencies": { "@sinclair/hammer": "^0.12.1", diff --git a/package.json b/package.json index 4daf40d..f7acdb4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sinclair/typebox", - "version": "0.17.7", + "version": "0.18.0", "description": "JSONSchema Type Builder with Static Type Resolution for TypeScript", "keywords": [ "json-schema", diff --git a/readme.md b/readme.md index abb62b1..7f27a4f 100644 --- a/readme.md +++ b/readme.md @@ -12,16 +12,16 @@ ## Install -##### Node +#### Node ```bash $ npm install @sinclair/typebox --save ``` -##### Deno +#### Deno ```typescript -import { Type, Static } from 'https://deno.land/x/typebox/src/typebox.ts' +import { Static, Type } from 'https://deno.land/x/typebox/src/typebox.ts' ``` ## Usage @@ -29,9 +29,9 @@ import { Type, Static } from 'https://deno.land/x/typebox/src/typebox.ts' ```typescript import { Static, Type } from '@sinclair/typebox' -const T = Type.String() // const T = { "type": "string" } +const T = Type.String() // const T = { "type": "string" } -type T = Static // type T = string +type T = Static // type T = string ``` @@ -40,9 +40,9 @@ type T = Static // type T = string TypeBox is a type builder library that creates in-memory JSON Schema objects that can be statically resolved to TypeScript types. The schemas produced by this library are built to match the static type checking rules of the TypeScript compiler. TypeBox allows one to create a single unified type that can be both statically checked by the TypeScript compiler and runtime asserted using standard JSON schema validation. -TypeBox can be used as a simple tool to build up complex schemas or integrated into RPC or REST services to help validate JSON data received over the wire. TypeBox does not provide any JSON schema validation. Please use libraries such as [AJV](https://www.npmjs.com/package/ajv) to validate schemas built with this library. +TypeBox can be used as a simple tool to build up complex schemas or integrated into RPC or REST services to help validate JSON data received over the wire. TypeBox does not provide any JSON schema validation. Please use libraries such as AJV to validate schemas built with this library. -Requires TypeScript 4.0.3 and above. +Targets JSON schema draft `2019-09`. Requires TypeScript 4.0.3 and above. License MIT @@ -57,7 +57,6 @@ License MIT - [Recursive Types](#Recursive-Types) - [Extended Types](#Extended-Types) - [Strict](#Strict) -- [Interfaces](#Interfaces) - [Validation](#Validation) @@ -68,7 +67,7 @@ The following demonstrates TypeBox's general usage. ```typescript -import { Type, Static } from '@sinclair/typebox' +import { Static, Type } from '@sinclair/typebox' //-------------------------------------------------------------------------------------------- // @@ -76,7 +75,7 @@ import { Type, Static } from '@sinclair/typebox' // //-------------------------------------------------------------------------------------------- -type Record = { +type T = { id: string, name: string, timestamp: number @@ -88,7 +87,7 @@ type Record = { // //-------------------------------------------------------------------------------------------- -const Record = Type.Object({ // const Record = { +const T = Type.Object({ // const T = { id: Type.String(), // type: 'object', name: Type.String(), // properties: { timestamp: Type.Integer() // id: { @@ -114,7 +113,7 @@ const Record = Type.Object({ // const Record = { // //-------------------------------------------------------------------------------------------- -type Record = Static // type Record = { +type T = Static // type T = { // id: string, // name: string, // timestamp: number @@ -126,12 +125,13 @@ type Record = Static // type Record = { // //-------------------------------------------------------------------------------------------- -function receive(record: Record) { // ... as a type - if(JSON.validate(Record, record)) { // ... as a schema +function receive(value: T) { // ... as a Type + + if(JSON.validate(T, value)) { // ... as a Schema + // ok... } } - ``` @@ -196,14 +196,6 @@ The following table outlines the TypeBox mappings between TypeScript and JSON sc │ │ │ } │ │ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ -│ const T = Type.Dict( │ type T = { │ const T = { │ -│ Type.Number() │ [key: string] : number │ type: 'object' │ -│ ) │ } │ additionalProperties: { │ -│ │ │ type: 'number' │ -│ │ │ } │ -│ │ │ } │ -│ │ │ │ -├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Object({ │ type T = { │ const T = { │ │ x: Type.Number(), │ x: number, │ type: 'object', │ │ y: Type.Number() │ y: number │ properties: { │ @@ -259,18 +251,34 @@ The following table outlines the TypeBox mappings between TypeScript and JSON sc │ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Intersect([ │ type T = { │ const T = { │ -│ Type.Object({ │ x: number │ type: 'object', │ -│ x: Type.Number() │ } & { │ properties: { │ -│ }), │ y: number │ x: { │ -│ Type.Object({ │ } │ type: 'number' │ -│ y: Type.Number() │ │ }, │ -│ }) │ │ y: { │ -│ }) │ │ type: 'number' │ -│ │ │ } │ -│ │ │ }, │ -│ │ │ required: ['x', 'y'] │ -│ │ │ } │ -│ │ │ │ +│ Type.Object({ │ x: number │ allOf: [{ │ +│ x: Type.Number() │ } & { │ type: 'object', │ +│ }), │ y: number │ properties: { │ +│ Type.Object({ │ } │ a: { │ +│ y: Type.Number() │ │ type: 'number' │ +│ }) │ │ } │ +│ }) │ │ }, │ +│ │ │ required: ['a'] │ +│ │ │ }, { │ +│ │ │ type: 'object', │ +│ │ │ properties: { │ +│ │ │ b: { │ +│ │ │ type: 'number' │ +│ │ │ } │ +│ │ │ }, │ +│ │ │ required: ['b'] │ +│ │ │ }] │ +│ │ │ } │ +│ │ │ │ +├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ +│ const T = Type.Record( │ type T = { │ const T = { │ +│ Type.String(), │ [key: string]: number │ type: 'object' │ +│ Type.Number() │ } │ patternProperties: { │ +│ ) │ │ '.*': { │ +│ │ │ type: 'number' │ +│ │ │ } │ +│ │ │ } │ +│ │ │ } │ │ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Partial( │ type T = Partial<{ │ const T = { │ @@ -560,106 +568,11 @@ const U = Type.Strict(T) // const U = { // } ``` - - -### Interfaces - -It is possible to create interfaces from TypeBox types. Consider the following code that creates a `ControllerInterface` type that has a single function `createRecord(...)`. The following is how one might approach this in TypeScript. - -```typescript -interface CreateRecordRequest { - data: string -} - -interface CreateRecordResponse { - id: string -} - -interface ControllerInterface { - createRecord(record: CreateRecordRequest): Promise -} - -class Controller implements ControllerInterface { - async createRecord(record: CreateRecordRequest): Promise { - return { id: '1' } - } -} -``` -The following is the TypeBox equivalent. -```typescript -import { Type, Static } from '@sinclair/typebox' - -type CreateRecordRequest = Static -const CreateRecordRequest = Type.Object({ - data: Type.String() -}) -type CreateRecordResponse = Static -const CreateRecordResponse = Type.Object({ - id: Type.String() -}) - -type ControllerInterface = Static -const ControllerInterface = Type.Object({ - createRecord: Type.Function([CreateRecordRequest], Type.Promise(CreateRecordResponse)) -}) - -class Controller implements ControllerInterface { - async createRecord(record: CreateRecordRequest): Promise { - return { id: '1' } - } -} -``` -Because TypeBox encodes the type information as JSON schema, it now becomes possible to reflect on the JSON schema to produce sharable metadata that can be used as machine readable documentation. -```typescript - -console.log(JSON.stringify(ControllerInterface, null, 2)) -// outputs: -// -// { -// "type": "object", -// "properties": { -// "createRecord": { -// "type": "function", -// "arguments": [ -// { -// "type": "object", -// "properties": { -// "data": { -// "type": "string" -// } -// }, -// "required": [ -// "data" -// ] -// } -// ], -// "returns": { -// "type": "promise", -// "item": { -// "type": "object", -// "properties": { -// "id": { -// "type": "string" -// } -// }, -// "required": [ -// "id" -// ] -// } -// } -// } -// }, -// "required": [ -// "createRecord" -// ] -// } -``` - ### Validation -TypeBox does not provide JSON schema validation out of the box and expects users to select an appropriate JSON schema validation library for their needs. TypeBox schemas should match JSON Schema draft 6 so any library capable of draft 6 should be fine. A good library to use for validation is [Ajv](https://www.npmjs.com/package/ajv). The following example shows setting up Ajv 7 to work with TypeBox. +TypeBox does not provide JSON schema validation out of the box and expects users to select an appropriate JSON schema validation library for their needs. TypeBox schemas should match JSON Schema draft `2019-09` so any library capable of draft `2019-09` should be fine. A good library to use for validation is [Ajv](https://www.npmjs.com/package/ajv). The following example shows setting up Ajv 7 to work with TypeBox. ```bash $ npm install ajv ajv-formats --save @@ -668,10 +581,11 @@ $ npm install ajv ajv-formats --save ```typescript import { Type } from '@sinclair/typebox' import addFormats from 'ajv-formats' -import Ajv from 'ajv' +import Ajv from 'ajv/dist/2019' -// Setup -const ajv = addFormats(new Ajv(), [ +const ajv = addFormats(new Ajv({ + allowUnionTypes: true +}), [ 'date-time', 'time', 'date', diff --git a/spec/schema/any.ts b/spec/schema/any.ts index 4181828..decdc26 100644 --- a/spec/schema/any.ts +++ b/spec/schema/any.ts @@ -2,13 +2,32 @@ import { Type } from '@sinclair/typebox' import { ok, fail } from './validate' describe("Any", () => { - it('Any', () => { - const T = Type.Any() - ok(T, null) - ok(T, {}) - ok(T, []) - ok(T, 1) - ok(T, true) - ok(T, 'hello') - }) + it('Should validate number', () => { + const T = Type.Any() + ok(T, 1) + }) + it('Should validate string', () => { + const T = Type.Any() + ok(T, 'hello') + }) + it('Should validate boolean', () => { + const T = Type.Any() + ok(T, true) + }) + it('Should validate array', () => { + const T = Type.Any() + ok(T, [1, 2, 3]) + }) + it('Should validate object', () => { + const T = Type.Any() + ok(T, { a: 1, b: 2 }) + }) + it('Should validate null', () => { + const T = Type.Any() + ok(T, null) + }) + it('Should validate undefined', () => { + const T = Type.Any() + ok(T, undefined) + }) }) diff --git a/spec/schema/array.ts b/spec/schema/array.ts index 93ff97b..6c4fcbd 100644 --- a/spec/schema/array.ts +++ b/spec/schema/array.ts @@ -2,49 +2,76 @@ import { Type } from '@sinclair/typebox' import { ok, fail } from './validate' describe("Array", () => { - it('Array ', () => { - const T = Type.Array(Type.Any()) - ok(T, []) - ok(T, [0, true, 'hello', {}]) - }) + it('Should validate an array of any', () => { + const T = Type.Array(Type.Any()) + ok(T, [0, true, 'hello', {}]) + }) - it('Array ', () => { - const A = Type.Number() - const T = Type.Array(A) - ok(T, []) - ok(T, [1, 2, 3, 4]) - fail(T, [true]) - fail(T, [null]) - }) + it('Should not validate varying array when item is number', () => { + const T = Type.Array(Type.Number()) + fail(T, [1, 2, 3, 'hello']) + }) - it('Array ', () => { - const A = Type.Number() - const B = Type.String() - const T = Type.Array(Type.Union([A, B])) - ok(T, []) - ok(T, [1, 'hello', 3, 'world']) - fail(T, [1, 'hello', 3, 'world', null]) - fail(T, [null]) - }) + it('Should validate for an array of unions', () => { + const T = Type.Array(Type.Union([Type.Number(), Type.String()])) + ok(T, [1, 'hello', 3, 'world']) + }) - it('Array ', () => { - const A = Type.Object({ a: Type.String() }) - const B = Type.Object({ b: Type.String() }) - const T = Type.Array(Type.Intersect([A, B])) - ok(T, []) - ok(T, [{a: 'hello', b: 'world'}]) - fail(T, [{a: 'hello'}]) - fail(T, [{b: 'world'}]) - fail(T, [null]) - }) + it('Should not validate for an array of unions where item is not in union.', () => { + const T = Type.Array(Type.Union([Type.Number(), Type.String()])) + fail(T, [1, 'hello', 3, 'world', true]) + }) - it('Array <[A, B]>', () => { - const A = Type.String() - const B = Type.Number() - const T = Type.Array(Type.Tuple([A, B])) - ok(T, []) - ok(T, [['hello', 42]]) - fail(T, [[42, 'hello']]) - fail(T, [null]) - }) + it('Should validate for an empty array', () => { + const T = Type.Array(Type.Union([Type.Number(), Type.String()])) + ok(T, []) + }) + + it('Should validate for an array of intersection types', () => { + const A = Type.Object({ a: Type.String() }) + const B = Type.Object({ b: Type.String() }) + const C = Type.Intersect([A, B], { unevaluatedProperties: false }) + const T = Type.Array(C) + ok(T, [ + { a: 'hello', b: 'hello' }, + { a: 'hello', b: 'hello' }, + { a: 'hello', b: 'hello' }, + ]) + }) + + it('Should not validate for an array of intersection types when passing unevaluated property', () => { + const A = Type.Object({ a: Type.String() }) + const B = Type.Object({ b: Type.String() }) + const C = Type.Intersect([A, B], { unevaluatedProperties: false }) + const T = Type.Array(C) + fail(T, [ + { a: 'hello', b: 'hello' }, + { a: 'hello', b: 'hello' }, + { a: 'hello', b: 'hello', c: 'additional' }, + ]) + }) + + it('Should validate an array of tuples', () => { + const A = Type.String() + const B = Type.Number() + const C = Type.Tuple([A, B]) + const T = Type.Array(C) + ok(T, [ + ['hello', 1], + ['hello', 1], + ['hello', 1], + ]) + }) + + it('Should not validate an array of tuples when tuple values are incorrect', () => { + const A = Type.String() + const B = Type.Number() + const C = Type.Tuple([A, B]) + const T = Type.Array(C) + fail(T, [ + [1, 'hello'], + [1, 'hello'], + [1, 'hello'], + ]) + }) }) diff --git a/spec/schema/boolean.ts b/spec/schema/boolean.ts index eead466..6ce2168 100644 --- a/spec/schema/boolean.ts +++ b/spec/schema/boolean.ts @@ -2,13 +2,40 @@ import { Type } from '@sinclair/typebox' import { ok, fail } from './validate' describe("Boolean", () => { - it('Boolean', () => { - const T = Type.Boolean() - ok(T, true) - fail(T, {}) - fail(T, []) - fail(T, 42) - fail(T, 'hello') - fail(T, null) - }) + + it('Should validate a boolean', () => { + const T = Type.Boolean() + ok(T, true) + ok(T, false) + }) + + it('Should not validate a number', () => { + const T = Type.Boolean() + fail(T, 1) + }) + + it('Should not validate a string', () => { + const T = Type.Boolean() + fail(T, 'true') + }) + + it('Should not validate an array', () => { + const T = Type.Boolean() + fail(T, [true]) + }) + + it('Should not validate an object', () => { + const T = Type.Boolean() + fail(T, {}) + }) + + it('Should not validate an null', () => { + const T = Type.Boolean() + fail(T, null) + }) + + it('Should not validate an undefined', () => { + const T = Type.Boolean() + fail(T, undefined) + }) }) diff --git a/spec/schema/box.ts b/spec/schema/box.ts index 9a15dd3..e37b3ee 100644 --- a/spec/schema/box.ts +++ b/spec/schema/box.ts @@ -1,9 +1,9 @@ import { Type } from '@sinclair/typebox' -import { createValidator } from './validate' - +import { validator } from './validate' describe("Box", () => { - it('Should validate with correct data', () => { + + it('Should should validate Vertex structure', () => { const Vector2 = Type.Object({ x: Type.Number(), y: Type.Number() }) const Vector3 = Type.Object({ x: Type.Number(), y: Type.Number(), z: Type.Number() }) const Vector4 = Type.Object({ x: Type.Number(), y: Type.Number(), z: Type.Number(), w: Type.Number() }) @@ -14,70 +14,68 @@ describe("Box", () => { uv: Type.Ref(Math3D, 'Vector2'), }) - const validator = createValidator().addSchema(Math3D) - const ok = validator.validate(Vertex, { + const ajv = validator().addSchema(Math3D) + const ok = ajv.validate(Vertex, { position: { x: 1, y: 1, z: 1, w: 1 }, - normal: { x: 1, y: 1, z: 1 }, - uv: { x: 1, y: 1 }, + normal: { x: 1, y: 1, z: 1 }, + uv: { x: 1, y: 1 }, }) - - - if (ok === false) throw Error('Expected success') + }) + it('Should not validate when Vertex structure is missing properties', () => { + const Vector2 = Type.Object({ x: Type.Number(), y: Type.Number() }) + const Vector3 = Type.Object({ x: Type.Number(), y: Type.Number(), z: Type.Number() }) + const Vector4 = Type.Object({ x: Type.Number(), y: Type.Number(), z: Type.Number(), w: Type.Number() }) + const Math3D = Type.Box('math3d', { Vector2, Vector3, Vector4 }) + const Vertex = Type.Object({ + position: Type.Ref(Math3D, 'Vector4'), + normal: Type.Ref(Math3D, 'Vector3'), + uv: Type.Ref(Math3D, 'Vector2'), + }) + const ajv = validator().addSchema(Math3D) + const ok = ajv.validate(Vertex, { + position: { x: 1, y: 1, z: 1, w: 1 }, + normal: { x: 1, y: 1, z: 1 }, + }) + if (ok === true) throw Error('Expected fail') + }) - }) - it('Should fail with missing property', () => { + it('Should not validate when Vertex structure contains invalid property values.', () => { const Vector2 = Type.Object({ x: Type.Number(), y: Type.Number() }) const Vector3 = Type.Object({ x: Type.Number(), y: Type.Number(), z: Type.Number() }) const Vector4 = Type.Object({ x: Type.Number(), y: Type.Number(), z: Type.Number(), w: Type.Number() }) const Math3D = Type.Box('math3d', { Vector2, Vector3, Vector4 }) const Vertex = Type.Object({ position: Type.Ref(Math3D, 'Vector4'), - normal: Type.Ref(Math3D, 'Vector3'), - uv: Type.Ref(Math3D, 'Vector2'), + normal: Type.Ref(Math3D, 'Vector3'), + uv: Type.Ref(Math3D, 'Vector2'), }) - const validator = createValidator().addSchema(Math3D) - const ok = validator.validate(Vertex, { + const ajv = validator().addSchema(Math3D) + const ok = ajv.validate(Vertex, { position: { x: 1, y: 1, z: 1, w: 1 }, - normal: { x: 1, y: 1, z: 1 }, + normal: { x: 1, y: 1, z: 1 }, + uv: { x: 1, y: 'not a number'}, }) if (ok === true) throw Error('Expected fail') }) - it('Should fail with invalid data', () => { + + it('Should not validate when Box has not been registered with validator (AJV)', () => { const Vector2 = Type.Object({ x: Type.Number(), y: Type.Number() }) const Vector3 = Type.Object({ x: Type.Number(), y: Type.Number(), z: Type.Number() }) const Vector4 = Type.Object({ x: Type.Number(), y: Type.Number(), z: Type.Number(), w: Type.Number() }) const Math3D = Type.Box('math3d', { Vector2, Vector3, Vector4 }) const Vertex = Type.Object({ position: Type.Ref(Math3D, 'Vector4'), - normal: Type.Ref(Math3D, 'Vector3'), - uv: Type.Ref(Math3D, 'Vector2'), + normal: Type.Ref(Math3D, 'Vector3'), + uv: Type.Ref(Math3D, 'Vector2'), }) - const validator = createValidator().addSchema(Math3D) - const ok = validator.validate(Vertex, { - position: { x: 1, y: 1, z: 1, w: 1 }, - normal: { x: 1, y: 1, z: 1 }, - uv: { x: 1, y: 'not a number'}, - }) - if (ok === true) throw Error('Expected fail') - }) - it('Should throw for non-registered box', () => { - const Vector2 = Type.Object({ x: Type.Number(), y: Type.Number() }) - const Vector3 = Type.Object({ x: Type.Number(), y: Type.Number(), z: Type.Number() }) - const Vector4 = Type.Object({ x: Type.Number(), y: Type.Number(), z: Type.Number(), w: Type.Number() }) - const Math3D = Type.Box('math3d', { Vector2, Vector3, Vector4 }) - const Vertex = Type.Object({ - position: Type.Ref(Math3D, 'Vector4'), - normal: Type.Ref(Math3D, 'Vector3'), - uv: Type.Ref(Math3D, 'Vector2'), - }) - const validator = createValidator() + const ajv = validator() let did_throw = false try { - validator.validate(Vertex, { + ajv.validate(Vertex, { position: { x: 1, y: 1, z: 1, w: 1 }, - normal: { x: 1, y: 1, z: 1 }, - uv: { x: 1, y: 1}, + normal: { x: 1, y: 1, z: 1 }, + uv: { x: 1, y: 1}, }) } catch { did_throw = true diff --git a/spec/schema/dict.ts b/spec/schema/dict.ts deleted file mode 100644 index 423e9ce..0000000 --- a/spec/schema/dict.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Type } from '@sinclair/typebox' -import { ok, fail } from './validate' - -describe('Dict', () => { - - it('Dict ', () => { - const T = Type.Dict(Type.Any()) - - ok(T, { key: 32 }) - ok(T, { key: 'hello' }) - ok(T, { key: true }) - ok(T, { key: {} }) - ok(T, { key: [] }) - ok(T, { key: null }) - }) - - it('Dict ', () => { - const A = Type.String() - const T = Type.Dict(A) - - ok(T, { key: 'hello' }) - fail(T, { key: 32 }) - fail(T, { key: true }) - fail(T, { key: {} }) - fail(T, { key: [] }) - fail(T, { key: null }) - }) - - it('Dict ', () => { - const A = Type.Number() - const B = Type.String() - const T = Type.Dict(Type.Union([A, B])) - - ok(T, { key: 'hello' }) - ok(T, { key: 32 }) - fail(T, { key: true }) - fail(T, { key: {} }) - fail(T, { key: [] }) - fail(T, { key: null }) - }) - - it('Dict ', () => { - const A = Type.Object({ a: Type.String() }) - const B = Type.Object({ b: Type.String() }) - const T = Type.Dict(Type.Intersect([A, B])) - - ok(T, { key: {a: 'hello', b: 'world'} }) - fail(T, { key: {b: 'world'} }) - fail(T, { key: {a: 'hello'} }) - fail(T, { key: true }) - fail(T, { key: {} }) - fail(T, { key: [] }) - fail(T, { key: null }) - }) - - it('Dict <[A, B]>', () => { - const A = Type.String() - const B = Type.Number() - const T = Type.Dict(Type.Tuple([A, B])) - - ok(T, { key: ['hello', 42] }) - fail(T, { key: [42, 'hello'] }) - fail(T, { key: {a: 'hello'} }) - fail(T, { key: {} }) - fail(T, { key: [] }) - fail(T, { key: [null] }) - }) -}) diff --git a/spec/schema/enum.ts b/spec/schema/enum.ts index 7b68db6..83931a2 100644 --- a/spec/schema/enum.ts +++ b/spec/schema/enum.ts @@ -2,53 +2,68 @@ import { Type } from '@sinclair/typebox' import { ok, fail } from './validate' import * as assert from 'assert' -describe('Enum', () => { - it('number enum', () => { - enum NumberEnum { - Foo, // = 0 - Bar // = 1 - } - const T = Type.Enum(NumberEnum) - fail(T, 'baz') - fail(T, 'Foo') - ok(T, 0) - ok(T, 1) - - assert.strictEqual(T.type, 'number') - }) +describe('Enum', () => { - it('string enum', () => { - enum StringEnum { - Foo = 'foo', - Bar = 'bar' - } - const T = Type.Enum(StringEnum) - fail(T, 'baz') - fail(T, 'Foo') - ok(T, 'foo') - ok(T, 'bar') - - assert.strictEqual(T.type, 'string') - }) + it('Should validate when emum uses default numeric values', () => { + enum Kind { + Foo, // = 0 + Bar // = 1 + } + const T = Type.Enum(Kind) + ok(T, 0) + ok(T, 1) + assert.strictEqual(T.type, 'number') + }) + it('Should not validate when given enum values are not numeric', () => { + enum Kind { + Foo, // = 0 + Bar // = 1 + } + const T = Type.Enum(Kind) + fail(T, 'Foo') + fail(T, 'Bar') + assert.strictEqual(T.type, 'number') + }) - it('mixed string|number enum', () => { - enum MixedEnum { - Foo, - Bar = 'bar' - } - const T = Type.Enum(MixedEnum) - fail(T, 'baz') - fail(T, 'Foo') - fail(T, 1) - ok(T, 0) - ok(T, 'bar') - - assert.deepStrictEqual(T.type, ['string', 'number']) - }) + it('Should validate when emum has defined string values', () => { + enum Kind { + Foo = 'foo', + Bar = 'bar' + } + const T = Type.Enum(Kind) + ok(T, 'foo') + ok(T, 'bar') + assert.strictEqual(T.type, 'string') + }) - it('empty enum', () => { - enum EmptyEnum {} - const T = Type.Enum(EmptyEnum) - assert.strictEqual(T.type, undefined) - }) + it('Should not validate when emum has defined string values and user passes numeric', () => { + enum Kind { + Foo = 'foo', + Bar = 'bar' + } + const T = Type.Enum(Kind) + fail(T, 0) + fail(T, 1) + assert.strictEqual(T.type, 'string') + }) + + it('Should validate when enum has one or more string values', () => { + enum Kind { + Foo, + Bar = 'bar' + } + const T = Type.Enum(Kind) + ok(T, 0) + ok(T, 'bar') + fail(T, 'baz') + fail(T, 'Foo') + fail(T, 1) + assert.deepStrictEqual(T.type, ['string', 'number']) + }) + + it('Should specify type undefined when enum is empty', () => { + enum Kind { } + const T = Type.Enum(Kind) + assert.strictEqual(T.type, undefined) + }) }) diff --git a/spec/schema/index.ts b/spec/schema/index.ts index fa18eb9..c92da7d 100644 --- a/spec/schema/index.ts +++ b/spec/schema/index.ts @@ -2,7 +2,6 @@ import './any' import './array' import './boolean' import './box' -import './dict' import './enum' import './intersect' import './keyof' @@ -12,9 +11,13 @@ import './null' import './number' import './object' import './omit' +import './optional' import './partial' import './pick' +import './readonly-optional' +import './readonly' import './rec' +import './record' import './regex' import './required' import './string' diff --git a/spec/schema/intersect.ts b/spec/schema/intersect.ts index 4d50a07..6ca69d6 100644 --- a/spec/schema/intersect.ts +++ b/spec/schema/intersect.ts @@ -2,54 +2,86 @@ import { Type } from '@sinclair/typebox' import { ok, fail } from './validate' describe('Intersect', () => { - it('A & B', () => { - const A = Type.Object({ a: Type.String() }) - const B = Type.Object({ b: Type.Number() }) - const T = Type.Intersect([A, B]) - - ok(T, {a: 'hello', b: 42 }) - fail(T, {a: 'hello' }) - fail(T, {b: 42 }) - }) - it('A & B & C', () => { - const A = Type.Object({ a: Type.String() }) - const B = Type.Object({ b: Type.Number() }) - const C = Type.Object({ c: Type.Boolean() }) - const T = Type.Intersect([A, B, C]) - - ok(T, {a: 'hello', b: 42, c: true }) - fail(T, {a: 'hello' }) - fail(T, {b: 42 }) - fail(T, {c: true }) - }) - describe('Additional Properties', () => { - const A = Type.Object({ - a: Type.String(), - b: Type.String(), + it('Should intersect two objects', () => { + const A = Type.Object({ a: Type.String() }) + const B = Type.Object({ b: Type.Number() }) + const T = Type.Intersect([A, B]) + ok(T, { a: 'hello', b: 42 }) }) - const B = Type.Object({ - c: Type.String(), - }) - const T = Type.Intersect([A, B], { additionalProperties: false }) - - ok(T, { a: '1', b: '2', c: '3' }) - fail(T, { a: '1', b: '2' }) - fail(T, { a: '1', b: '2', c: '3', d: '4' }) - }) - describe('Duplicate Required', () => { - const A = Type.Object({ - a: Type.String(), + it('Should allow additional properties if not using unevaluatedProperties', () => { + const A = Type.Object({ a: Type.String() }) + const B = Type.Object({ b: Type.Number() }) + const T = Type.Intersect([A, B]) + ok(T, { a: 'hello', b: 42, c: true }) }) - const B = Type.Object({ - a: Type.String(), - b: Type.String() - }) - const T = Type.Intersect([A, B]) - ok(T, { a: "1", b: "2" }) - fail(T, { a: "1" }) - fail(T, { b: "2" }) - }) + it('Should not allow additional properties if using unevaluatedProperties', () => { + const A = Type.Object({ a: Type.String() }) + const B = Type.Object({ b: Type.Number() }) + const T = Type.Intersect([A, B], { unevaluatedProperties: false }) + fail(T, { a: 'hello', b: 42, c: true }) + }) + + describe('Should not allow unevaluatedProperties with record intersection', () => { + const A = Type.Object({ + a: Type.String(), + b: Type.String(), + c: Type.String() + }) + const B = Type.Record(Type.Number(), Type.Number()) + const T = Type.Intersect([A, B]) + ok(T, { + a: 'a', b: 'b', c: 'c', + 0: 1, 1: 2, 2: 3 + }) + }) + + describe('Should intersect object with number record', () => { + const A = Type.Object({ + a: Type.String(), + b: Type.String(), + c: Type.String() + }) + const B = Type.Record(Type.Number(), Type.Number()) + const T = Type.Intersect([A, B]) + ok(T, { + a: 'a', b: 'b', c: 'c', + 0: 1, 1: 2, 2: 3 + }) + }) + + describe('Should not intersect object with string record', () => { + const A = Type.Object({ + a: Type.String(), + b: Type.String(), + c: Type.String() + }) + const B = Type.Record(Type.String(), Type.Number()) + const T = Type.Intersect([A, B]) + fail(T, { + a: 'a', b: 'b', c: 'c', + x: 1, y: 2, z: 3 + }) + }) + + describe('Should intersect object with union literal record', () => { + const A = Type.Object({ + a: Type.String(), + b: Type.String(), + c: Type.String() + }) + const K = Type.Union([ + Type.Literal('x'), + Type.Literal('y'), + Type.Literal('z') + ]) + const B = Type.Record(K, Type.Number()) + const T = Type.Intersect([A, B]) + ok(T, { + a: 'a', b: 'b', c: 'c', + x: 1, y: 2, z: 3 + }) + }) }) diff --git a/spec/schema/keyof.ts b/spec/schema/keyof.ts index ebb4b05..66de941 100644 --- a/spec/schema/keyof.ts +++ b/spec/schema/keyof.ts @@ -2,29 +2,15 @@ import { Type } from '@sinclair/typebox' import { ok, fail } from './validate' describe("KeyOf", () => { - it('Flat', () => { - const T = Type.KeyOf(Type.Object({ - x: Type.Number(), - y: Type.Number(), - z: Type.Number(), - })) - - ok(T, 'x') - ok(T, 'y') - ok(T, 'z') - fail(T, 'w') - }) - it('Nested', () => { - const T = Type.Object({ - k: Type.KeyOf(Type.Object({ + it('Should validate with all object keys as a kind of union', () => { + const T = Type.KeyOf(Type.Object({ x: Type.Number(), y: Type.Number(), - z: Type.Number(), + z: Type.Number() })) + ok(T, 'x') + ok(T, 'y') + ok(T, 'z') + fail(T, 'w') }) - ok(T, { k: 'x' }) - ok(T, { k: 'y' }) - ok(T, { k: 'z' }) - fail(T, { k: 'w' }) - }) }) diff --git a/spec/schema/literal.ts b/spec/schema/literal.ts index 328235b..72d190c 100644 --- a/spec/schema/literal.ts +++ b/spec/schema/literal.ts @@ -3,41 +3,42 @@ import { ok, fail } from './validate' import * as assert from 'assert' describe("Literal", () => { - it('Number', () => { - const T = Type.Literal(42) - ok(T, 42) - - fail(T, {}) - fail(T, []) - fail(T, 43) - fail(T, 'world') - fail(T, null) - - assert.strictEqual(T.type, 'number') - }) - it('Boolean', () => { - const T = Type.Literal(true) - ok(T, true) - - fail(T, false) - fail(T, {}) - fail(T, []) - fail(T, 43) - fail(T, 'world') - fail(T, null) - - assert.strictEqual(T.type, 'boolean') - }) - it('String', () => { - const T = Type.Literal('hello') - ok(T, 'hello') + it('Should validate literal number', () => { + const T = Type.Literal(42) + ok(T, 42) + }) + it('Should validate literal string', () => { + const T = Type.Literal('hello') + ok(T, 'hello') + }) - fail(T, {}) - fail(T, []) - fail(T, 42) - fail(T, 'world') - fail(T, null) - - assert.strictEqual(T.type, 'string') - }) + it('Should validate literal boolean', () => { + const T = Type.Literal(true) + ok(T, true) + }) + + it('Should not validate invalid literal number', () => { + const T = Type.Literal(42) + fail(T, 43) + }) + it('Should not validate invalid literal string', () => { + const T = Type.Literal('hello') + fail(T, 'world') + }) + it('Should not validate invalid literal boolean', () => { + const T = Type.Literal(false) + fail(T, true) + }) + + it('Should validate literal union', () => { + const T = Type.Union([Type.Literal(42), Type.Literal('hello')]) + ok(T, 42) + ok(T, 'hello') + }) + + it('Should not validate invalid literal union', () => { + const T = Type.Union([Type.Literal(42), Type.Literal('hello')]) + fail(T, 43) + fail(T, 'world') + }) }) diff --git a/spec/schema/modifier.ts b/spec/schema/modifier.ts index 8018baf..456b725 100644 --- a/spec/schema/modifier.ts +++ b/spec/schema/modifier.ts @@ -2,22 +2,21 @@ import { Type, ReadonlyModifier, OptionalModifier } from '@sinclair/typebox' import * as assert from 'assert' describe('Modifier', () => { - it('Omit modifier', () => { + it('Omit modifier', () => { + const T = Type.Object({ + a: Type.Readonly(Type.String()), + b: Type.Optional(Type.String()), + }) - const T = Type.Object({ - a: Type.Readonly(Type.String()), - b: Type.Optional(Type.String()), + const S = JSON.stringify(T) + const P = JSON.parse(S) as any + + // check assignment on Type + assert.equal(T.properties.a['modifier'], ReadonlyModifier) + assert.equal(T.properties.b['modifier'], OptionalModifier) + + // check deserialized + assert.equal(P.properties.a['modifier'], undefined) + assert.equal(P.properties.b['modifier'], undefined) }) - - const S = JSON.stringify(T) - const P = JSON.parse(S) as any - - // check assignment on Type - assert.equal(T.properties.a['modifier'], ReadonlyModifier) - assert.equal(T.properties.b['modifier'], OptionalModifier) - - // check deserialized - assert.equal(P.properties.a['modifier'], undefined) - assert.equal(P.properties.b['modifier'], undefined) - }) }) diff --git a/spec/schema/null.ts b/spec/schema/null.ts index 9c81413..3ea7e88 100644 --- a/spec/schema/null.ts +++ b/spec/schema/null.ts @@ -2,13 +2,38 @@ import { Type } from '@sinclair/typebox' import { ok, fail } from './validate' describe("Null", () => { - it('Null', () => { - const T = Type.Null() - ok(T, null) - fail(T, {}) - fail(T, []) - fail(T, 1) - fail(T, true) - fail(T, 'hello') - }) + it('Should not validate number', () => { + const T = Type.Null() + fail(T, 1) + }) + + it('Should not validate string', () => { + const T = Type.Null() + fail(T, 'hello') + }) + + it('Should not validate boolean', () => { + const T = Type.Null() + fail(T, true) + }) + + it('Should not validate array', () => { + const T = Type.Null() + fail(T, [1, 2, 3]) + }) + + it('Should not validate object', () => { + const T = Type.Null() + fail(T, { a: 1, b: 2 }) + }) + + it('Should not validate null', () => { + const T = Type.Null() + ok(T, null) + }) + + it('Should not validate undefined', () => { + const T = Type.Null() + fail(T, undefined) + }) }) diff --git a/spec/schema/number.ts b/spec/schema/number.ts index b3b917c..a37af99 100644 --- a/spec/schema/number.ts +++ b/spec/schema/number.ts @@ -2,13 +2,32 @@ import { Type } from '@sinclair/typebox' import { ok, fail } from './validate' describe("Number", () => { - it('Number', () => { - const T = Type.Number() - ok(T, 42) - fail(T, {}) - fail(T, []) - fail(T, 'hello') - fail(T, true) - fail(T, null) - }) + it('Should validate number', () => { + const T = Type.Number() + ok(T, 1) + }) + it('Should not validate string', () => { + const T = Type.Number() + fail(T, 'hello') + }) + it('Should not validate boolean', () => { + const T = Type.Number() + fail(T, true) + }) + it('Should not validate array', () => { + const T = Type.Number() + fail(T, [1, 2, 3]) + }) + it('Should not validate object', () => { + const T = Type.Number() + fail(T, { a: 1, b: 2 }) + }) + it('Should not validate null', () => { + const T = Type.Number() + fail(T, null) + }) + it('Should not validate undefined', () => { + const T = Type.Number() + fail(T, undefined) + }) }) diff --git a/spec/schema/object.ts b/spec/schema/object.ts index a4718f4..2d0582c 100644 --- a/spec/schema/object.ts +++ b/spec/schema/object.ts @@ -3,91 +3,60 @@ import { Type } from '@sinclair/typebox' import { ok, fail } from './validate' describe('Object', () => { - it('Object', () => { - const T = Type.Object({ - a: Type.Number(), - b: Type.String(), - c: Type.Boolean(), - d: Type.Array(Type.Number()), - e: Type.Object({ x: Type.Number(), y: Type.Number() }) + it('Should validate with correct property values', () => { + const T = Type.Object({ + a: Type.Number(), + b: Type.String(), + c: Type.Boolean(), + d: Type.Array(Type.Number()), + e: Type.Object({ x: Type.Number(), y: Type.Number() }) + }) + ok(T, { + a: 10, + b: 'hello', + c: true, + d: [1, 2, 3], + e: { x: 10, y: 20 } + }) + }) + it('Should not validate with incorrect property values', () => { + const T = Type.Object({ + a: Type.Number(), + b: Type.String(), + c: Type.Boolean(), + d: Type.Array(Type.Number()), + e: Type.Object({ x: Type.Number(), y: Type.Number() }) + }) + fail(T, { + a: 'not a number', // error + b: 'hello', + c: true, + d: [1, 2, 3], + e: { x: 10, y: 20 } + }) }) - ok(T, { a: 10, b: '', c: true, d: [1, 2, 3], e: { x: 10, y: 20 } }) - fail(T, {}) - fail(T, []) - fail(T, 'hello') - fail(T, true) - fail(T, 123) - fail(T, null) - }) - it('Optional', () => { - const T = Type.Object({ - a: Type.Optional(Type.Number()), - b: Type.Optional(Type.String()), - c: Type.Optional(Type.Boolean()), - d: Type.Optional(Type.Array(Type.Number())), - e: Type.Optional(Type.Object({ x: Type.Number(), y: Type.Number() })) - }, { additionalProperties: false }) + it('Should allow additionalProperties by default', () => { + const T = Type.Object({ + a: Type.Number(), + b: Type.String() + }) + ok(T, { + a: 1, + b: 'hello', + c: true + }) + }) - ok(T, { a: 10, b: '', c: true, d: [1, 2, 3], e: { x: 10, y: 20 } }) - ok(T, {}) - - fail(T, { z: null }) // should this fail? - fail(T, []) - fail(T, 'hello') - fail(T, true) - fail(T, 123) - fail(T, null) - }) - - it('Readonly', () => { - const T = Type.Object({ - a: Type.Readonly(Type.Number()), - b: Type.Readonly(Type.String()), - c: Type.Readonly(Type.Boolean()), - d: Type.Readonly(Type.Array(Type.Number())), - e: Type.Readonly(Type.Object({ x: Type.Number(), y: Type.Number() })) - }, { additionalProperties: false }) - - ok(T, { a: 10, b: '', c: true, d: [1, 2, 3], e: { x: 10, y: 20 } }) - - fail(T, {}) - fail(T, []) - fail(T, 'hello') - fail(T, true) - fail(T, 123) - fail(T, null) - }) - - describe('Required', () => { - it('Contains all non-optional properties', () => { - const T = Type.Object({ - a: Type.String(), - b: Type.Optional(Type.String()), - c: Type.String(), - }); - - deepStrictEqual(T.required, ['a', 'c']); - }); - - it(`The 'required' array property is omitted when all properties are optional`, () => { - const T = Type.Object({ - a: Type.Optional(Type.String()), - b: Type.Optional(Type.String()), - c: Type.Optional(Type.String()), - }) - strictEqual(T.required, undefined); - }); - }); - - describe('Additional Properties', () => { - const T = Type.Object({ - a: Type.String(), - b: Type.String(), - c: Type.String(), - }, { additionalProperties: false }); - ok(T, { a: '1', b: '2', c: '3' }) - fail(T, { a: '1', b: '2' }) - fail(T, { a: '1', b: '2', c: '3', d: '4' }) - }) + it('Should not allow additionalProperties if additionalProperties is false', () => { + const T = Type.Object({ + a: Type.Number(), + b: Type.String() + }, { additionalProperties: false }) + fail(T, { + a: 1, + b: 'hello', + c: true + }) + }) }) diff --git a/spec/schema/omit.ts b/spec/schema/omit.ts index cdc414c..29398ec 100644 --- a/spec/schema/omit.ts +++ b/spec/schema/omit.ts @@ -3,34 +3,34 @@ import { ok, fail } from './validate' import { strictEqual } from 'assert' describe('Omit', () => { - it('Vector3 to Vector2', () => { - const Vector3 = Type.Object({ - x: Type.Number(), - y: Type.Number(), - z: Type.Number() - }) - const Vector2 = Type.Omit(Vector3, ['z']) - ok(Vector2, { x: 1, y: 1 }) - }) - - it('User', () => { - const User = Type.Object({ - id: Type.Readonly(Type.Integer()), - name: Type.String({ default: null }), - email: Type.String({ default: undefined }), - }); - const PartialUser = Type.Omit(User, ['id']) - ok(PartialUser, { name: 'user', email: 'user@example.com' }) - }) - - it('Options', () => { - const Vector3 = Type.Object({ + it('Should omit properties on the source schema', () => { + const A = Type.Object({ x: Type.Number(), y: Type.Number(), z: Type.Number() - }, { title: 'Vector3' }) - const Vector2 = Type.Omit(Vector3, ['z'], { title: 'Vector2' }) - strictEqual(Vector3.title, 'Vector3') - strictEqual(Vector2.title, 'Vector2') + }, { additionalProperties: false }) + const T = Type.Omit(A, ['z']) + ok(T, { x: 1, y: 1 }) + }) + + it('Should remove required properties on the target schema', () => { + const A = Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number() + }, { additionalProperties: false }) + const T = Type.Omit(A, ['z']) + strictEqual(T.required!.includes('z'), false) + }) + + it('Should inherit options from the source object', () => { + const A = Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number() + }, { additionalProperties: false }) + const T = Type.Omit(A, ['z']) + strictEqual(A.additionalProperties, false) + strictEqual(T.additionalProperties, false) }) }) diff --git a/spec/schema/optional.ts b/spec/schema/optional.ts new file mode 100644 index 0000000..07168d9 --- /dev/null +++ b/spec/schema/optional.ts @@ -0,0 +1,21 @@ +import { deepStrictEqual, strictEqual } from 'assert' +import { Type } from '@sinclair/typebox' +import { ok, fail } from './validate' + +describe('Optional', () => { + it('Should validate object with optional', () => { + const T = Type.Object({ + a: Type.Optional(Type.String()), + b: Type.String() + }, { additionalProperties: false }) + ok(T, { a: 'hello', b: 'world' }) + ok(T, { b: 'world' }) + }) + it('Should remove required value from schema', () => { + const T = Type.Object({ + a: Type.Optional(Type.String()), + b: Type.String() + }, { additionalProperties: false }) + strictEqual(T.required!.includes('a'), false) + }) +}) diff --git a/spec/schema/partial.ts b/spec/schema/partial.ts index f1f0f42..41c1a0e 100644 --- a/spec/schema/partial.ts +++ b/spec/schema/partial.ts @@ -1,45 +1,44 @@ -import { OptionalModifier, ReadonlyOptionalModifier, Type } from '@sinclair/typebox' -import { ok, fail } from './validate' +import { OptionalModifier, ReadonlyOptionalModifier, Type } from '@sinclair/typebox' +import { ok, fail } from './validate' import { strictEqual } from 'assert' describe('Partial', () => { - - it('Required to Partial', () => { - const Required = Type.Object({ + + it('Should convert a required object into a partial.', () => { + const A = Type.Object({ x: Type.Number(), y: Type.Number(), z: Type.Number() - }) - const Partial = Type.Partial(Required) - ok(Required, { x: 1, y: 1, z: 1 }) - ok(Partial, {}) - ok(Partial, { x: 1, y: 1, z: 1 }) - ok(Partial, { x: 1, y: 1 }) - ok(Partial, { x: 1 }) + }, { additionalProperties: false }) + const T = Type.Partial(A) + ok(T, { x: 1, y: 1, z: 1 }) + ok(T, { x: 1, y: 1 }) + ok(T, { x: 1 }) + ok(T, {}) }) - it('Modifiers', () => { - const T = Type.Object({ + it('Should update modifier types correctly when converting to partial', () => { + const A = Type.Object({ x: Type.ReadonlyOptional(Type.Number()), y: Type.Readonly(Type.Number()), z: Type.Optional(Type.Number()), w: Type.Number() - }) - const U = Type.Partial(T) - strictEqual(U.properties.x.modifier, ReadonlyOptionalModifier) - strictEqual(U.properties.y.modifier, ReadonlyOptionalModifier) - strictEqual(U.properties.z.modifier, OptionalModifier) - strictEqual(U.properties.w.modifier, OptionalModifier) + }, { additionalProperties: false }) + const T = Type.Partial(A) + strictEqual(T.properties.x.modifier, ReadonlyOptionalModifier) + strictEqual(T.properties.y.modifier, ReadonlyOptionalModifier) + strictEqual(T.properties.z.modifier, OptionalModifier) + strictEqual(T.properties.w.modifier, OptionalModifier) }) - it('Options', () => { - const Required = Type.Object({ + it('Should inherit options from the source object', () => { + const A = Type.Object({ x: Type.Number(), y: Type.Number(), z: Type.Number() - }, { title: 'Required' }) - const Partial = Type.Partial(Required, { title: 'Partial' }) - strictEqual(Required.title, 'Required') - strictEqual(Partial.title, 'Partial') + }, { additionalProperties: false }) + const T = Type.Partial(A) + strictEqual(A.additionalProperties, false) + strictEqual(T.additionalProperties, false) }) }) diff --git a/spec/schema/pick.ts b/spec/schema/pick.ts index 6fa4b89..33a8a0d 100644 --- a/spec/schema/pick.ts +++ b/spec/schema/pick.ts @@ -3,24 +3,34 @@ import { ok, fail } from './validate' import { strictEqual } from 'assert' describe('Pick', () => { - it('Vector3 to Vector2', () => { - const Vector3 = Type.Object({ - x: Type.Number(), - y: Type.Number(), - z: Type.Number() - }) - const Vector2 = Type.Pick(Vector3, ['x', 'y']) - ok(Vector2, { x: 1, y: 1 }) - }) - - it('Options', () => { + it('Should pick properties from the source schema', () => { const Vector3 = Type.Object({ x: Type.Number(), y: Type.Number(), z: Type.Number() - }, { title: 'Vector3' }) - const Vector2 = Type.Pick(Vector3, ['x', 'y'], { title: 'Vector2' }) - strictEqual(Vector3.title, 'Vector3') - strictEqual(Vector2.title, 'Vector2') + }, { additionalProperties: false }) + const T = Type.Pick(Vector3, ['x', 'y']) + ok(T, { x: 1, y: 1 }) + }) + + it('Should remove required properties on the target schema', () => { + const A = Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number() + }, { additionalProperties: false }) + const T = Type.Pick(A, ['x', 'y']) + strictEqual(T.required!.includes('z'), false) + }) + + it('Should inherit options from the source object', () => { + const A = Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number() + }, { additionalProperties: false }) + const T = Type.Pick(A, ['x', 'y']) + strictEqual(A.additionalProperties, false) + strictEqual(T.additionalProperties, false) }) }) diff --git a/spec/schema/readonly-optional.ts b/spec/schema/readonly-optional.ts new file mode 100644 index 0000000..4edb7c9 --- /dev/null +++ b/spec/schema/readonly-optional.ts @@ -0,0 +1,21 @@ +import { Type } from '@sinclair/typebox' +import { ok, fail } from './validate' +import { strictEqual } from 'assert' + +describe('ReadonlyOptional', () => { + it('Should validate object with optional', () => { + const T = Type.Object({ + a: Type.ReadonlyOptional(Type.String()), + b: Type.String() + }, { additionalProperties: false }) + ok(T, { a: 'hello', b: 'world' }) + ok(T, { b: 'world' }) + }) + it('Should remove required value from schema', () => { + const T = Type.Object({ + a: Type.ReadonlyOptional(Type.String()), + b: Type.String() + }, { additionalProperties: false }) + strictEqual(T.required!.includes('a'), false) + }) +}) diff --git a/spec/schema/readonly.ts b/spec/schema/readonly.ts new file mode 100644 index 0000000..8cdc0f8 --- /dev/null +++ b/spec/schema/readonly.ts @@ -0,0 +1,23 @@ +import { deepStrictEqual, strictEqual } from 'assert' +import { Type } from '@sinclair/typebox' +import { ok, fail } from './validate' + +describe('Readonly', () => { + + it('Should validate object with readonly', () => { + const T = Type.Object({ + a: Type.Readonly(Type.String()), + b: Type.Readonly(Type.String()) + }, { additionalProperties: false }) + ok(T, { a: 'hello', b: 'world' }) + }) + + it('Should retain required array on object', () => { + const T = Type.Object({ + a: Type.Readonly(Type.String()), + b: Type.Readonly(Type.String()), + }, { additionalProperties: false }) + strictEqual(T.required!.includes('a'), true) + strictEqual(T.required!.includes('b'), true) + }) +}) \ No newline at end of file diff --git a/spec/schema/rec.ts b/spec/schema/rec.ts index 9e98ec3..930bcb0 100644 --- a/spec/schema/rec.ts +++ b/spec/schema/rec.ts @@ -2,11 +2,12 @@ import { Type } from '@sinclair/typebox' import { ok, fail } from './validate' describe("Rec", () => { - it('Should validate recursive node type', () => { + + it('Should validate recursive Node type', () => { const Node = Type.Rec(Self => Type.Object({ id: Type.String(), nodes: Type.Array(Self) - })) + }), 'Node') ok(Node, { id: '1', nodes: [] @@ -20,9 +21,17 @@ describe("Rec", () => { { id: '5', nodes: [] } ] }) + }) + + it('Should not validate recursive Node type', () => { + const Node = Type.Rec(Self => Type.Object({ + id: Type.String(), + nodes: Type.Array(Self) + }), 'Node') fail(Node, { id: '1', - nodes: 'a string' // not assert on any + nodes: [1, 2, 3, 4] }) }) + }) diff --git a/spec/schema/record.ts b/spec/schema/record.ts new file mode 100644 index 0000000..85b6eca --- /dev/null +++ b/spec/schema/record.ts @@ -0,0 +1,90 @@ +import { Type } from '@sinclair/typebox' +import { ok, fail } from './validate' + +describe('Record', () => { + + it('Should validate when all property values are numbers', () => { + const T = Type.Record(Type.String(), Type.Number()) + ok(T, { 'a': 1, 'b': 2, 'c': 3 }) + }) + + it('Should validate when all property keys are strings', () => { + const T = Type.Record(Type.String(), Type.Number()) + ok(T, { 'a': 1, 'b': 2, 'c': 3, '0': 4 }) + }) + + it('Should validate when all property keys are numbers', () => { + const T = Type.Record(Type.Number(), Type.Number()) + ok(T, { '0': 1, '1': 2, '2': 3, '3': 4 }) + }) + + it('Should validate when all property keys are numbers, but one property is a string with varying type', () => { + const T = Type.Record(Type.Number(), Type.Number()) + ok(T, { '0': 1, '1': 2, '2': 3, '3': 4, 'a': 'hello' }) + }) + + it('Should not validate if passing a leading zeros for numeric keys', () => { + const T = Type.Record(Type.Number(), Type.Number(), { additionalProperties: false }) + fail(T, { + '00': 1, + '01': 2, + '02': 3, + '03': 4 + }) + }) + + it('Should not validate if passing a signed numeric keys', () => { + const T = Type.Record(Type.Number(), Type.Number(), { additionalProperties: false }) + fail(T, { + '-0': 1, + '-1': 2, + '-2': 3, + '-3': 4 + }) + }) + + it('Should not validate when all property keys are numbers, but one property is a string with varying type with additionalProperties false', () => { + const T = Type.Record(Type.Number(), Type.Number(), { additionalProperties: false }) + fail(T, { '0': 1, '1': 2, '2': 3, '3': 4, 'a': 'hello' }) + }) + + it('Should validate when specifying union literals for the known keys', () => { + const K = Type.Union([ + Type.Literal('a'), + Type.Literal('b'), + Type.Literal('c'), + ]) + const T = Type.Record(K, Type.Number()) + ok(T, { a: 1, b: 2, c: 3, d: 'hello' }) + }) + + it('Should not validate when specifying union literals for the known keys and with additionalProperties: false', () => { + const K = Type.Union([ + Type.Literal('a'), + Type.Literal('b'), + Type.Literal('c'), + ]) + const T = Type.Record(K, Type.Number(), { additionalProperties: false }) + fail(T, { a: 1, b: 2, c: 3, d: 'hello' }) + }) + + it('Should should validate when specifying regular expressions', () => { + const K = Type.RegEx(/^op_.*$/) + const T = Type.Record(K, Type.Number(), { additionalProperties: false }) + ok(T, { + 'op_a': 1, + 'op_b': 2, + 'op_c': 3, + }) + }) + + it('Should should not validate when specifying regular expressions and passing invalid property', () => { + const K = Type.RegEx(/^op_.*$/) + const T = Type.Record(K, Type.Number(), { additionalProperties: false }) + fail(T, { + 'op_a': 1, + 'op_b': 2, + 'aop_c': 3, + }) + }) +}) diff --git a/spec/schema/regex.ts b/spec/schema/regex.ts index 3ef138c..af2d523 100644 --- a/spec/schema/regex.ts +++ b/spec/schema/regex.ts @@ -3,14 +3,29 @@ import { ok, fail } from './validate' describe('RegEx', () => { - it('Numeric', () => { - const T = Type.RegEx(/[012345]/) - ok(T, '0') - ok(T, '1') - ok(T, '2') - ok(T, '3') - ok(T, '4') - ok(T, '5') - fail(T, '6') - }) + it('Should validate numeric value', () => { + const T = Type.RegEx(/[012345]/) + ok(T, '0') + ok(T, '1') + ok(T, '2') + ok(T, '3') + ok(T, '4') + ok(T, '5') + }) + + it('Should validate true or false string value', () => { + const T = Type.RegEx(/true|false/) + ok(T, 'true') + ok(T, 'true') + ok(T, 'true') + ok(T, 'false') + ok(T, 'false') + ok(T, 'false') + fail(T, '6') + }) + + it('Should not validate failed regex test', () => { + const T = Type.RegEx(/true|false/) + fail(T, 'unknown') + }) }) diff --git a/spec/schema/required.ts b/spec/schema/required.ts index 3a76bb2..9491902 100644 --- a/spec/schema/required.ts +++ b/spec/schema/required.ts @@ -1,43 +1,44 @@ import { Type, ReadonlyModifier, ReadonlyOptionalModifier, OptionalModifier } from '@sinclair/typebox' -import { ok, fail } from './validate' +import { ok, fail } from './validate' import { strictEqual } from 'assert' describe('Required', () => { - it('Partial to Required', () => { - const Partial = Type.Object({ + it('Should convert a partial object into a required object', () => { + const A = Type.Object({ x: Type.Optional(Type.Number()), y: Type.Optional(Type.Number()), z: Type.Optional(Type.Number()) - }) - const Required = Type.Required(Partial) - ok(Partial, { }) - ok(Required, { x: 1, y: 1, z: 1 }) - fail(Required, { }) + }, { additionalProperties: false }) + const T = Type.Required(A) + ok(T, { x: 1, y: 1, z: 1 }) + fail(T, { x: 1, y: 1 }) + fail(T, { x: 1 }) + fail(T, {}) }) - it('Modifiers', () => { - const T = Type.Object({ + it('Should update modifier types correctly when converting to required', () => { + const A = Type.Object({ x: Type.ReadonlyOptional(Type.Number()), y: Type.Readonly(Type.Number()), z: Type.Optional(Type.Number()), w: Type.Number() }) - const U = Type.Required(T) - strictEqual(U.properties.x.modifier, ReadonlyModifier) - strictEqual(U.properties.y.modifier, ReadonlyModifier) - strictEqual(U.properties.z.modifier, undefined) - strictEqual(U.properties.w.modifier, undefined) + const T = Type.Required(A) + strictEqual(T.properties.x.modifier, ReadonlyModifier) + strictEqual(T.properties.y.modifier, ReadonlyModifier) + strictEqual(T.properties.z.modifier, undefined) + strictEqual(T.properties.w.modifier, undefined) }) - it('Options', () => { - const Partial = Type.Object({ + it('Should inherit options from the source object', () => { + const A = Type.Object({ x: Type.Optional(Type.Number()), y: Type.Optional(Type.Number()), z: Type.Optional(Type.Number()) - }, { title: 'Partial' }) - const Required = Type.Required(Partial, { title: 'Required' }) - strictEqual(Partial.title, 'Partial') - strictEqual(Required.title, 'Required') + }, { additionalPropeties: false }) + const T = Type.Required(A) + strictEqual(A.additionalPropeties, false) + strictEqual(T.additionalPropeties, false) }) }) diff --git a/spec/schema/string.ts b/spec/schema/string.ts index e49c680..c51bb1c 100644 --- a/spec/schema/string.ts +++ b/spec/schema/string.ts @@ -2,39 +2,48 @@ import { Type } from '@sinclair/typebox' import { ok, fail } from './validate' describe("String", () => { + it('Should not validate number', () => { + const T = Type.String() + fail(T, 1) + }) + it('Should validate string', () => { + const T = Type.String() + ok(T, 'hello') + }) + it('Should not validate boolean', () => { + const T = Type.String() + fail(T, true) + }) + it('Should not validate array', () => { + const T = Type.String() + fail(T, [1, 2, 3]) + }) + it('Should not validate object', () => { + const T = Type.String() + fail(T, { a: 1, b: 2 }) + }) + it('Should not validate null', () => { + const T = Type.String() + fail(T, null) + }) + it('Should not validate undefined', () => { + const T = Type.String() + fail(T, undefined) + }) - it('String', () => { - const T = Type.String() - ok(T, 'hello') - fail(T, {}) - fail(T, []) - fail(T, 1) - fail(T, true) - fail(T, null) - }) + it('Should validate string format as email', () => { + const T = Type.String({ format: 'email' }) + ok(T, 'name@domain.com') + }) - it('DateTime', () => { - const T = Type.String({ format: 'date-time' }) - ok(T, "2018-11-13T20:20:39+00:00") - fail(T, "2018-11-13") - fail(T, "20:20:39+00:00") - fail(T, "string") - }) + it('Should validate string format as uuid', () => { + const T = Type.String({ format: 'uuid' }) + ok(T, '4a7a17c9-2492-4a53-8e13-06ea2d3f3bbf') + }) - it('Email', () => { - const T = Type.String({ format: 'email' }) - ok(T, "dave@domain.com") - fail(T, "orange") - }) - - it('Uuid', () => { - const T = Type.String({ format: 'uuid' }) - ok(T, 'f1b35107-5a79-4108-8ff8-470087865b9c') - ok(T, '67e147cf-c47b-472d-9cbe-b85177d791b6') - ok(T, 'db8cd64f-b297-4ef1-a0ba-298322965247') - ok(T, '30957e77-40c4-4b05-8d4d-64f63b83034d') - ok(T, 'abe8f990-370e-479a-b452-851ae15714dc') - ok(T, '4f08d994-cafe-4075-a9ca-bedc8a49427b') - fail(T, 'orange') - }) + it('Should validate string format as iso8601 date', () => { + const T = Type.String({ format: 'date-time' }) + ok(T, '2021-06-11T20:30:00-04:00') + }) }) + diff --git a/spec/schema/tuple.ts b/spec/schema/tuple.ts index 63a0fb6..c499d0c 100644 --- a/spec/schema/tuple.ts +++ b/spec/schema/tuple.ts @@ -1,24 +1,60 @@ import { Type } from '@sinclair/typebox' +import { isRegExp } from 'util' import { ok, fail } from './validate' describe('Tuple', () => { - it('[A, B]', () => { - const A = Type.Object({ a: Type.String() }) - const B = Type.Object({ b: Type.Number() }) - const T = Type.Tuple([A, B]) + it('Should validate tuple of [string, number]', () => { + const A = Type.String() + const B = Type.Number() + const T = Type.Tuple([A, B]) + ok(T, ['hello', 42]) + }) - ok(T, [{ a: 'hello' }, { b: 42 }]) - fail(T, [{ b: 42 }, { a: 'hello' }]) - }) + it('Should not validate tuple of [string, number] when reversed', () => { + const A = Type.String() + const B = Type.Number() + const T = Type.Tuple([A, B]) + fail(T, [42, 'hello']) + }) - it('[A, B, C]', () => { - const A = Type.Object({ a: Type.String() }) - const B = Type.Object({ b: Type.Number() }) - const C = Type.Object({ c: Type.Boolean() }) - const T = Type.Tuple([A, B, C]) + it('Should validate tuple of objects', () => { + const A = Type.Object({ a: Type.String() }) + const B = Type.Object({ b: Type.Number() }) + const T = Type.Tuple([A, B]) + ok(T, [ + { a: 'hello' }, + { b: 42 }, + ]) + }) - ok(T, [{ a: 'hello' }, { b: 42 }, { c: true }]) - fail(T, [{ c: true }, { a: 'hello' }, { b: 42 }]) - }) + it('Should not validate tuple of objects when reversed', () => { + const A = Type.Object({ a: Type.String() }) + const B = Type.Object({ b: Type.Number() }) + const T = Type.Tuple([A, B]) + fail(T, [ + { b: 42 }, + { a: 'hello' }, + ]) + }) + + it('Should not validate tuple when array is less than tuple length', () => { + const A = Type.Object({ a: Type.String() }) + const B = Type.Object({ b: Type.Number() }) + const T = Type.Tuple([A, B]) + fail(T, [ + { a: 'hello' }, + ]) + }) + + it('Should not validate tuple when array is greater than tuple length', () => { + const A = Type.Object({ a: Type.String() }) + const B = Type.Object({ b: Type.Number() }) + const T = Type.Tuple([A, B]) + fail(T, [ + { a: 'hello' }, + { b: 42 }, + { b: 42 }, + ]) + }) }) diff --git a/spec/schema/union.ts b/spec/schema/union.ts index 4d8e3ce..c1e3c85 100644 --- a/spec/schema/union.ts +++ b/spec/schema/union.ts @@ -3,55 +3,55 @@ import { ok, fail } from './validate' describe('Union', () => { - it('number | string', () => { - const A = Type.String() - const B = Type.Number() - const T = Type.Union([A, B]) + it('Should validate union of string, number and boolean', () => { + const A = Type.String() + const B = Type.Number() + const C = Type.Boolean() + const T = Type.Union([A, B, C]) + ok(T, 'hello') + ok(T, true) + ok(T, 42) + }) - fail(T, {}) - ok(T, 'hello') - ok(T, 42) - }) + it('Should validate union of objects', () => { + const A = Type.Object({ a: Type.String() }, { additionalProperties: false }) + const B = Type.Object({ b: Type.String() }, { additionalProperties: false }) + const T = Type.Union([A, B]) + ok(T, { a: 'hello' }) + ok(T, { b: 'world' }) + }) - it('A | (A & B)', () => { - const A = Type.Object({ a: Type.String() }) - const B = Type.Object({ a: Type.String(), b: Type.Optional(Type.Number()) }) - const T = Type.Union([A, B]) + it('Should validate union of objects where properties overlap', () => { + const A = Type.Object({ a: Type.String() }, { additionalProperties: false }) + const B = Type.Object({ a: Type.String(), b: Type.String() }, { additionalProperties: false }) + const T = Type.Union([A, B]) + ok(T, { a: 'hello' }) // A + ok(T, { a: 'hello', b: 'world' }) // B + }) + - fail(T, {}) - ok(T, { a: 'hello', b: 42 }) - ok(T, { a: 'hello' }) - }) + it('Should validate union of overlapping property of varying type', () => { + const A = Type.Object({ a: Type.String(), b: Type.Number() }, { additionalProperties: false }) + const B = Type.Object({ a: Type.String(), b: Type.String() }, { additionalProperties: false }) + const T = Type.Union([A, B]) + ok(T, { a: 'hello', b: 42 }) // A + ok(T, { a: 'hello', b: 'world' }) // B + }) - it('A | B | C', () => { - const A = Type.Object({ a: Type.String() }) - const B = Type.Object({ b: Type.Number() }) - const C = Type.Object({ c: Type.Boolean() }) - const T = Type.Union([A, B, C]) + it('Should validate union of literal strings', () => { + const A = Type.Literal('hello') + const B = Type.Literal('world') + const T = Type.Union([A, B]) + ok(T, 'hello') // A + ok(T, 'world') // B + }) - fail(T, {}) - ok(T, { a: 'hello' }) - ok(T, { b: 42 }) - ok(T, { c: true }) - }) - - it('A | B (tagged union)', () => { - const T = Type.Union([ - Type.Object({ - kind: Type.Literal('string'), - value: Type.String() - }), - Type.Object({ - kind: Type.Literal('number'), - value: Type.Number() - }) - ]) - - ok (T, { kind: 'string', value: 'hello' }) - ok (T, { kind: 'number', value: 1 }) - - fail(T, { kind: 'string', value: 1 }) - fail(T, { kind: 'number', value: 'hello' }) - }) + it('Should not validate union of literal strings for unknown string', () => { + const A = Type.Literal('hello') + const B = Type.Literal('world') + const T = Type.Union([A, B]) + fail(T, 'foo') // A + fail(T, 'bar') // B + }) }) diff --git a/spec/schema/unknown.ts b/spec/schema/unknown.ts index 474bf47..aea8b40 100644 --- a/spec/schema/unknown.ts +++ b/spec/schema/unknown.ts @@ -2,13 +2,32 @@ import { Type } from '@sinclair/typebox' import { ok, fail } from './validate' describe("Unknown", () => { - it('Unknown', () => { - const T = Type.Unknown() - ok(T, null) - ok(T, {}) - ok(T, []) - ok(T, 1) - ok(T, true) - ok(T, 'hello') - }) + it('Should validate number', () => { + const T = Type.Any() + ok(T, 1) + }) + it('Should validate string', () => { + const T = Type.Any() + ok(T, 'hello') + }) + it('Should validate boolean', () => { + const T = Type.Any() + ok(T, true) + }) + it('Should validate array', () => { + const T = Type.Any() + ok(T, [1, 2, 3]) + }) + it('Should validate object', () => { + const T = Type.Any() + ok(T, { a: 1, b: 2 }) + }) + it('Should validate null', () => { + const T = Type.Any() + ok(T, null) + }) + it('Should validate undefined', () => { + const T = Type.Any() + ok(T, undefined) + }) }) diff --git a/spec/schema/validate.ts b/spec/schema/validate.ts index e92f05b..37cc642 100644 --- a/spec/schema/validate.ts +++ b/spec/schema/validate.ts @@ -1,80 +1,65 @@ import { TSchema } from '@sinclair/typebox' import addFormats from 'ajv-formats' -import Ajv from 'ajv' +import Ajv from 'ajv/dist/2019' -const ajv = addFormats(new Ajv(), [ - 'date-time', - 'time', - 'date', - 'email', - 'hostname', - 'ipv4', - 'ipv6', - 'uri', - 'uri-reference', - 'uuid', - 'uri-template', - 'json-pointer', - 'relative-json-pointer', - 'regex' -]) -.addKeyword('kind') -.addKeyword('modifier') +export function validator() { + return addFormats(new Ajv({ + allowUnionTypes: true + }), [ + 'date-time', + 'time', + 'date', + 'email', + 'hostname', + 'ipv4', + 'ipv6', + 'uri', + 'uri-reference', + 'uuid', + 'uri-template', + 'json-pointer', + 'relative-json-pointer', + 'regex' + ]) + .addKeyword('kind') + .addKeyword('modifier') +} export function ok(type: T, data: unknown) { - const result = ajv.validate(type, data) as boolean - if (result === false) { - console.log('---------------------------') - console.log('type') - console.log('---------------------------') - console.log(JSON.stringify(type, null, 2)) - console.log('---------------------------') - console.log('data') - console.log('---------------------------') - console.log(JSON.stringify(data, null, 2)) - console.log('---------------------------') - console.log('errors') - console.log('---------------------------') - console.log(ajv.errorsText(ajv.errors)) - throw Error('expected ok') - } + const ajv = validator() + const result = ajv.validate(type, data) as boolean + if (result === false) { + console.log('---------------------------') + console.log('type') + console.log('---------------------------') + console.log(JSON.stringify(type, null, 2)) + console.log('---------------------------') + console.log('data') + console.log('---------------------------') + console.log(JSON.stringify(data, null, 2)) + console.log('---------------------------') + console.log('errors') + console.log('---------------------------') + console.log(ajv.errorsText(ajv.errors)) + throw Error('expected ok') + } } export function fail(type: T, data: unknown) { - const result = ajv.validate(type, data) as boolean - if (result === true) { - console.log('---------------------------') - console.log('type') - console.log('---------------------------') - console.log(JSON.stringify(type, null, 2)) - console.log('---------------------------') - console.log('data') - console.log('---------------------------') - console.log(JSON.stringify(data, null, 2)) - console.log('---------------------------') - throw Error('expected fail') - } -} - -export function createValidator() { - return addFormats(new Ajv(), [ - 'date-time', - 'time', - 'date', - 'email', - 'hostname', - 'ipv4', - 'ipv6', - 'uri', - 'uri-reference', - 'uuid', - 'uri-template', - 'json-pointer', - 'relative-json-pointer', - 'regex' - ]) - .addKeyword('kind') - .addKeyword('modifier') + const ajv = validator() + const result = ajv.validate(type, data) as boolean + if (result === true) { + console.log('---------------------------') + console.log('type') + console.log('---------------------------') + console.log(JSON.stringify(type, null, 2)) + console.log('---------------------------') + console.log('data') + console.log('---------------------------') + console.log(JSON.stringify(data, null, 2)) + console.log('---------------------------') + throw Error('expected fail') + } } \ No newline at end of file diff --git a/spec/static/any.ts b/spec/static/any.ts index 61bfbde..25d5b35 100644 --- a/spec/static/any.ts +++ b/spec/static/any.ts @@ -3,7 +3,7 @@ import { Type, Static } from '@sinclair/typebox' // -------------------------------------------- const T0 = Type.Any() -const F0 = (arg: Static) => {} +const F0 = (arg: Static) => { } F0('string') F0(true) F0({}) diff --git a/spec/static/array.ts b/spec/static/array.ts index e6a593c..d21ff17 100644 --- a/spec/static/array.ts +++ b/spec/static/array.ts @@ -3,11 +3,11 @@ import { Type, Static } from '@sinclair/typebox' // -------------------------------------------- const T0 = Type.Array(Type.String()) -const F0 = (arg: Static) => {} +const F0 = (arg: Static) => { } F0(['', '']) // -------------------------------------------- const T1 = Type.Array(Type.Union([Type.String(), Type.Number()])) -const F1 = (arg: Static) => {} +const F1 = (arg: Static) => { } F1(['', 1]) diff --git a/spec/static/boolean.ts b/spec/static/boolean.ts index 3992033..f64e9ae 100644 --- a/spec/static/boolean.ts +++ b/spec/static/boolean.ts @@ -3,6 +3,6 @@ import { Type, Static } from '@sinclair/typebox' // -------------------------------------------- const T0 = Type.Boolean() -const F0 = (arg: Static) => {} +const F0 = (arg: Static) => { } F0(true) diff --git a/spec/static/box.ts b/spec/static/box.ts index b7e0cec..5d3faca 100644 --- a/spec/static/box.ts +++ b/spec/static/box.ts @@ -8,7 +8,7 @@ const B0 = Type.Box('ns', { T0 }) const R0 = Type.Ref(B0, 'T0') -const F0 = (arg: Static) => {} +const F0 = (arg: Static) => { } F0([1, 2]) diff --git a/spec/static/dict.ts b/spec/static/dict.ts deleted file mode 100644 index 476c442..0000000 --- a/spec/static/dict.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Type, Static } from '@sinclair/typebox' - -// -------------------------------------------- - -const T0 = Type.Dict(Type.Union([Type.String(), Type.Number()])) -const F0 = (arg: Static) => {} -F0({ - 'a': 'hello', - 'b': 'world', - 'c': 1 -}) - diff --git a/spec/static/enum.ts b/spec/static/enum.ts index c28ccc1..8200db3 100644 --- a/spec/static/enum.ts +++ b/spec/static/enum.ts @@ -4,7 +4,7 @@ import { Type, Static } from '@sinclair/typebox' enum K0 { A, B, C } const T0 = Type.Enum(K0) -const F0 = (arg: Static) => {} +const F0 = (arg: Static) => { } F0(K0.A) F0(K0.B) F0(K0.C) @@ -13,7 +13,7 @@ F0(K0.C) enum K1 { A = '1', B = '2', C = '3' } const T1 = Type.Enum(K1) -const F1 = (arg: Static) => {} +const F1 = (arg: Static) => { } F1(K1.A) F1(K1.B) F1(K1.C) diff --git a/spec/static/index.ts b/spec/static/index.ts index fa18eb9..c92da7d 100644 --- a/spec/static/index.ts +++ b/spec/static/index.ts @@ -2,7 +2,6 @@ import './any' import './array' import './boolean' import './box' -import './dict' import './enum' import './intersect' import './keyof' @@ -12,9 +11,13 @@ import './null' import './number' import './object' import './omit' +import './optional' import './partial' import './pick' +import './readonly-optional' +import './readonly' import './rec' +import './record' import './regex' import './required' import './string' diff --git a/spec/static/intersect.ts b/spec/static/intersect.ts index 84597be..f88e642 100644 --- a/spec/static/intersect.ts +++ b/spec/static/intersect.ts @@ -12,10 +12,11 @@ const U1 = Type.Object({ }) const T0 = Type.Intersect([U0, U1]) -const F0 = (arg: Static) => {} +const F0 = (arg: Static) => { } F0({ a: 'string', b: 1, c: true }) +// -------------------------------------------- diff --git a/spec/static/keyof.ts b/spec/static/keyof.ts index f585da9..09bc75c 100644 --- a/spec/static/keyof.ts +++ b/spec/static/keyof.ts @@ -11,7 +11,7 @@ const U0 = Type.Object({ const T0 = Type.KeyOf(U0) -const F0 = (arg: Static) => {} +const F0 = (arg: Static) => { } F0('a') F0('c') F0('c') @@ -28,8 +28,8 @@ const T1 = Type.Object({ k: Type.KeyOf(U1) }) -const F1 = (arg: Static) => {} -F1({ k: 'a'}) -F1({ k: 'b'}) -F1({ k: 'c'}) -F1({ k: 'd'}) +const F1 = (arg: Static) => { } +F1({ k: 'a' }) +F1({ k: 'b' }) +F1({ k: 'c' }) +F1({ k: 'd' }) diff --git a/spec/static/literal.ts b/spec/static/literal.ts index 4f12148..4024206 100644 --- a/spec/static/literal.ts +++ b/spec/static/literal.ts @@ -3,17 +3,17 @@ import { Type, Static } from '@sinclair/typebox' // -------------------------------------------- const T0 = Type.Literal('string') -const F0 = (arg: Static) => {} +const F0 = (arg: Static) => { } F0('string') // -------------------------------------------- const T1 = Type.Literal(123) -const F1 = (arg: Static) => {} +const F1 = (arg: Static) => { } F1(123) // -------------------------------------------- const T2 = Type.Literal(true) -const F2 = (arg: Static) => {} +const F2 = (arg: Static) => { } F2(true) \ No newline at end of file diff --git a/spec/static/modifier.ts b/spec/static/modifier.ts index 5c2111f..c3dfacd 100644 --- a/spec/static/modifier.ts +++ b/spec/static/modifier.ts @@ -5,7 +5,7 @@ const T0 = Type.Object({ b: Type.Readonly(Type.String()), c: Type.ReadonlyOptional(Type.Boolean()) }) -const F0 = (arg: Static) => {} +const F0 = (arg: Static) => { } F0({ b: 'hello', diff --git a/spec/static/null.ts b/spec/static/null.ts index b0d660f..47d3c6d 100644 --- a/spec/static/null.ts +++ b/spec/static/null.ts @@ -3,6 +3,6 @@ import { Type, Static } from '@sinclair/typebox' // -------------------------------------------- const T0 = Type.Null() -const F0 = (arg: Static) => {} +const F0 = (arg: Static) => { } F0(null) diff --git a/spec/static/number.ts b/spec/static/number.ts index 1665a39..0d31080 100644 --- a/spec/static/number.ts +++ b/spec/static/number.ts @@ -3,6 +3,6 @@ import { Type, Static } from '@sinclair/typebox' // -------------------------------------------- const T0 = Type.Number() -const F0 = (arg: Static) => {} +const F0 = (arg: Static) => { } F0(1) diff --git a/spec/static/object.ts b/spec/static/object.ts index b99a63b..24cd0dc 100644 --- a/spec/static/object.ts +++ b/spec/static/object.ts @@ -9,17 +9,17 @@ const T0 = Type.Object({ d: Type.Object({ e: Type.Array(Type.String()), }), - e: Type.Dict(Type.String()), + e: Type.Record(Type.String(), Type.String()), }) -const F0 = (arg: Static) => {} +const F0 = (arg: Static) => { } F0({ a: 1, b: '', c: true, - d: { - e: [''] + d: { + e: [''] }, e: { a: '' } }) diff --git a/spec/static/omit.ts b/spec/static/omit.ts index 2719afe..e669be3 100644 --- a/spec/static/omit.ts +++ b/spec/static/omit.ts @@ -8,5 +8,5 @@ const T0 = Type.Omit(Type.Object({ z: Type.Number() }), ['z']) -const F0 = (arg: Static) => {} +const F0 = (arg: Static) => { } F0({ x: 1, y: 1 }) diff --git a/spec/static/optional.ts b/spec/static/optional.ts new file mode 100644 index 0000000..d24b624 --- /dev/null +++ b/spec/static/optional.ts @@ -0,0 +1,19 @@ +import { Type, Static } from '@sinclair/typebox' + +// -------------------------------------------- + +const T0 = Type.Object({ + a: Type.Number(), + b: Type.Optional(Type.Number()) +}) + +const F0 = (arg: Static) => { } + +F0({ + a: 1, + b: 1, +}) + +F0({ + a: 1 +}) diff --git a/spec/static/partial.ts b/spec/static/partial.ts index 37c18cf..1da049c 100644 --- a/spec/static/partial.ts +++ b/spec/static/partial.ts @@ -8,7 +8,7 @@ const T0 = Type.Partial(Type.Object({ z: Type.Number() })) -const F0 = (arg: Static) => {} +const F0 = (arg: Static) => { } F0({ x: 1, y: 1, z: 1 }) F0({ x: 1, y: 1 }) F0({ x: 1 }) diff --git a/spec/static/pick.ts b/spec/static/pick.ts index c4d0ab2..2f50427 100644 --- a/spec/static/pick.ts +++ b/spec/static/pick.ts @@ -8,5 +8,5 @@ const T0 = Type.Pick(Type.Object({ z: Type.Number() }), ['x', 'y']) -const F0 = (arg: Static) => {} +const F0 = (arg: Static) => { } F0({ x: 1, y: 1 }) \ No newline at end of file diff --git a/spec/static/readonly-optional.ts b/spec/static/readonly-optional.ts new file mode 100644 index 0000000..e69de29 diff --git a/spec/static/readonly.ts b/spec/static/readonly.ts new file mode 100644 index 0000000..e69de29 diff --git a/spec/static/rec.ts b/spec/static/rec.ts index a56c709..66bb6f0 100644 --- a/spec/static/rec.ts +++ b/spec/static/rec.ts @@ -3,18 +3,18 @@ import { Type, Static } from '@sinclair/typebox' // -------------------------------------------- const T0 = Type.Rec(Self => Type.Number()) -const F0 = (arg: Static) => {} +const F0 = (arg: Static) => { } F0(1) const T1 = Type.Rec(Self => Type.Object({ id: Type.String(), nodes: Type.Array(Self) })) -const F1 = (arg: Static) => {} +const F1 = (arg: Static) => { } F1({ id: '1', nodes: [{ - id: '2', + id: '2', nodes: [] as Static[] }] as Static[] }) diff --git a/spec/static/record.ts b/spec/static/record.ts new file mode 100644 index 0000000..d62a51c --- /dev/null +++ b/spec/static/record.ts @@ -0,0 +1,42 @@ +import { Type, Static } from '@sinclair/typebox' + +// -------------------------------------------- + +const T0 = Type.Record(Type.String(), Type.Number()) +const F0 = (arg: Static) => { } +F0({ + 'a': 1, + 'b': 2, + 'c': 3 +}) + +// -------------------------------------------- + +const T1 = Type.Record(Type.Number(), Type.Number()) +const F1 = (arg: Static) => { } +F1({ + '0': 1, + '1': 2, + '2': 3 +}) + +// -------------------------------------------- + +const K = Type.Union([ + Type.Literal('a'), + Type.Literal('b'), + Type.Literal('c'), + Type.Literal(0), + Type.Literal(1), + Type.Literal(2), +]) +const T2 = Type.Record(K, Type.Number()) +const F2 = (arg: Static) => { } +F2({ + 'a': 1, + 'b': 2, + 'c': 3, + 0: 1, + 1: 2, + 2: 3 +}) \ No newline at end of file diff --git a/spec/static/regex.ts b/spec/static/regex.ts index edfb350..de6f7e4 100644 --- a/spec/static/regex.ts +++ b/spec/static/regex.ts @@ -3,6 +3,6 @@ import { Type, Static } from '@sinclair/typebox' // -------------------------------------------- const T0 = Type.RegEx(/(.*)/g) -const F0 = (arg: Static) => {} +const F0 = (arg: Static) => { } F0('') diff --git a/spec/static/required.ts b/spec/static/required.ts index 6d0f310..a6941df 100644 --- a/spec/static/required.ts +++ b/spec/static/required.ts @@ -8,6 +8,6 @@ const T0 = Type.Required(Type.Object({ z: Type.Optional(Type.Number()) })) -const F0 = (arg: Static) => {} +const F0 = (arg: Static) => { } F0({ x: 1, y: 1, z: 1 }) diff --git a/spec/static/string.ts b/spec/static/string.ts index d6930b4..f5e53b9 100644 --- a/spec/static/string.ts +++ b/spec/static/string.ts @@ -3,6 +3,6 @@ import { Type, Static } from '@sinclair/typebox' // -------------------------------------------- const T0 = Type.String() -const F0 = (arg: Static) => {} +const F0 = (arg: Static) => { } F0('') diff --git a/spec/static/tuple.ts b/spec/static/tuple.ts index 91e6c41..3274a28 100644 --- a/spec/static/tuple.ts +++ b/spec/static/tuple.ts @@ -3,6 +3,6 @@ import { Type, Static } from '@sinclair/typebox' // -------------------------------------------- const T0 = Type.Tuple([Type.String(), Type.Boolean(), Type.Number()]) -const F0 = (arg: Static) => {} +const F0 = (arg: Static) => { } F0(['', true, 1]) diff --git a/spec/static/unknown.ts b/spec/static/unknown.ts index 2743979..eda3076 100644 --- a/spec/static/unknown.ts +++ b/spec/static/unknown.ts @@ -3,7 +3,7 @@ import { Type, Static } from '@sinclair/typebox' // -------------------------------------------- const T0 = Type.Unknown() -const F0 = (arg: Static) => {} +const F0 = (arg: Static) => { } F0('string') F0(true) F0({}) diff --git a/src/typebox.ts b/src/typebox.ts index 4dc259c..bc2f02e 100644 --- a/src/typebox.ts +++ b/src/typebox.ts @@ -30,9 +30,9 @@ THE SOFTWARE. // Modifiers // ------------------------------------------------------------------------ -export const ReadonlyOptionalModifier = Symbol('ReadonlyOptionalModifier') -export const OptionalModifier = Symbol('OptionalModifier') -export const ReadonlyModifier = Symbol('ReadonlyModifier') +export const ReadonlyOptionalModifier = Symbol('ReadonlyOptionalModifier') +export const OptionalModifier = Symbol('OptionalModifier') +export const ReadonlyModifier = Symbol('ReadonlyModifier') export type TModifier = TReadonlyOptional | TOptional | TReadonly export type TReadonlyOptional = T & { modifier: typeof ReadonlyOptionalModifier } @@ -43,22 +43,24 @@ export type TReadonly = T & { modifier: typeof Readon // Schema Standard // ------------------------------------------------------------------------ -export const BoxKind = Symbol('BoxKind') -export const KeyOfKind = Symbol('KeyOfKind') -export const UnionKind = Symbol('UnionKind') -export const TupleKind = Symbol('TupleKind') -export const ObjectKind = Symbol('ObjectKind') -export const DictKind = Symbol('DictKind') -export const ArrayKind = Symbol('ArrayKind') -export const EnumKind = Symbol('EnumKind') -export const LiteralKind = Symbol('LiteralKind') -export const StringKind = Symbol('StringKind') -export const NumberKind = Symbol('NumberKind') -export const IntegerKind = Symbol('IntegerKind') -export const BooleanKind = Symbol('BooleanKind') -export const NullKind = Symbol('NullKind') -export const UnknownKind = Symbol('UnknownKind') -export const AnyKind = Symbol('AnyKind') +export const BoxKind = Symbol('BoxKind') +export const KeyOfKind = Symbol('KeyOfKind') +export const IntersectKind = Symbol('IntersectKind') +export const UnionKind = Symbol('UnionKind') +export const TupleKind = Symbol('TupleKind') +export const ObjectKind = Symbol('ObjectKind') +export const RecordIndexKind = Symbol('RecordIndexed') +export const RecordKind = Symbol('RecordKind') +export const ArrayKind = Symbol('ArrayKind') +export const EnumKind = Symbol('EnumKind') +export const LiteralKind = Symbol('LiteralKind') +export const StringKind = Symbol('StringKind') +export const NumberKind = Symbol('NumberKind') +export const IntegerKind = Symbol('IntegerKind') +export const BooleanKind = Symbol('BooleanKind') +export const NullKind = Symbol('NullKind') +export const UnknownKind = Symbol('UnknownKind') +export const AnyKind = Symbol('AnyKind') export interface CustomOptions { title?: string @@ -97,7 +99,11 @@ export type NumberOptions = { multipleOf?: number } & CustomOptions -export type DictOptions = { +export type IntersectOptions = { + unevaluatedProperties?: boolean +} & CustomOptions + +export type IndexedOptions = { minProperties?: number maxProperties?: number } & CustomOptions @@ -106,28 +112,30 @@ export type ObjectOptions = { additionalProperties?: boolean } & CustomOptions -export type TEnumType = Record -export type TKey = string | number | symbol -export type TValue = string | number | boolean +export type TEnumType = Record +export type TKey = string | number | symbol +export type TValue = string | number | boolean +export type TRecordKey = TString | TNumber | TUnion[]> -export type TDefinitions = { [key: string]: TSchema } -export type TProperties = { [key: string]: TSchema } -export type TBox = { kind: typeof BoxKind, $id: string, definitions: T } -export type TTuple = { kind: typeof TupleKind, type: 'array', items: [...T], additionalItems: false, minItems: number, maxItems: number } & CustomOptions -export type TObject = { kind: typeof ObjectKind, type: 'object', properties: T, required?: string[] } & ObjectOptions -export type TUnion = { kind: typeof UnionKind, anyOf: [...T] } & CustomOptions -export type TKeyOf = { kind: typeof KeyOfKind, type: 'string', enum: [...T] } & CustomOptions -export type TDict = { kind: typeof DictKind, type: 'object', additionalProperties: T } & DictOptions -export type TArray = { kind: typeof ArrayKind, type: 'array', items: T } & ArrayOptions -export type TLiteral = { kind: typeof LiteralKind, const: T } & CustomOptions -export type TEnum = { kind: typeof EnumKind, type?: 'string' | 'number' | ['string', 'number'], enum: T[] } & CustomOptions -export type TString = { kind: typeof StringKind, type: 'string' } & StringOptions -export type TNumber = { kind: typeof NumberKind, type: 'number' } & NumberOptions -export type TInteger = { kind: typeof IntegerKind, type: 'integer' } & NumberOptions -export type TBoolean = { kind: typeof BooleanKind, type: 'boolean' } & CustomOptions -export type TNull = { kind: typeof NullKind, type: 'null' } & CustomOptions -export type TUnknown = { kind: typeof UnknownKind } & CustomOptions -export type TAny = { kind: typeof AnyKind } & CustomOptions +export type TDefinitions = { [key: string]: TSchema } +export type TProperties = { [key: string]: TSchema } +export type TBox = { kind: typeof BoxKind, $id: string, definitions: T } +export type TTuple = { kind: typeof TupleKind, type: 'array', items: [...T], additionalItems: false, minItems: number, maxItems: number } & CustomOptions +export type TObject = { kind: typeof ObjectKind, type: 'object', properties: T, required?: string[] } & ObjectOptions +export type TUnion = { kind: typeof UnionKind, anyOf: [...T] } & CustomOptions +export type TIntersect = { kind: typeof IntersectKind, type: 'object', allOf: [...T] } & IntersectOptions +export type TKeyOf = { kind: typeof KeyOfKind, type: 'string', enum: [...T] } & CustomOptions +export type TRecord = { kind: typeof RecordKind, type: 'object', patternProperties: { [pattern: string]: T } } & ObjectOptions +export type TArray = { kind: typeof ArrayKind, type: 'array', items: T } & ArrayOptions +export type TLiteral = { kind: typeof LiteralKind, const: T } & CustomOptions +export type TEnum = { kind: typeof EnumKind, type?: 'string' | 'number' | ['string', 'number'], enum: T[] } & CustomOptions +export type TString = { kind: typeof StringKind, type: 'string' } & StringOptions +export type TNumber = { kind: typeof NumberKind, type: 'number' } & NumberOptions +export type TInteger = { kind: typeof IntegerKind, type: 'integer' } & NumberOptions +export type TBoolean = { kind: typeof BooleanKind, type: 'boolean' } & CustomOptions +export type TNull = { kind: typeof NullKind, type: 'null' } & CustomOptions +export type TUnknown = { kind: typeof UnknownKind } & CustomOptions +export type TAny = { kind: typeof AnyKind } & CustomOptions // ------------------------------------------------------------------------ // Schema Extended @@ -141,19 +149,20 @@ export const VoidKind = Symbol('VoidKind') export type TConstructor = { kind: typeof ConstructorKind, type: 'constructor', arguments: readonly [...T], returns: U } & CustomOptions export type TFunction = { kind: typeof FunctionKind, type: 'function', arguments: readonly [...T], returns: U } & CustomOptions export type TPromise = { kind: typeof PromiseKind, type: 'promise', item: T } & CustomOptions -export type TUndefined = { kind: typeof UndefinedKind, type: 'undefined' } & CustomOptions -export type TVoid = { kind: typeof VoidKind, type: 'void' } & CustomOptions +export type TUndefined = { kind: typeof UndefinedKind, type: 'undefined' } & CustomOptions +export type TVoid = { kind: typeof VoidKind, type: 'void' } & CustomOptions // ------------------------------------------------------------------------ // Schema // ------------------------------------------------------------------------ export type TSchema = + | TIntersect | TUnion | TTuple | TObject | TKeyOf - | TDict + | TRecord | TArray | TEnum | TLiteral @@ -193,9 +202,8 @@ export type TPartial = { // Static Inference // ------------------------------------------------------------------------ -export type UnionToIntersect = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never -export type IntersectObjectArray = T extends Array> ? UnionToIntersect : TProperties -export type ObjectPropertyKeys = T extends TObject ? PropertyKeys : never +export type UnionToIntersect = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never +export type ObjectPropertyKeys = T extends TObject ? PropertyKeys : never export type PropertyKeys = keyof T export type ReadonlyOptionalPropertyKeys = { [K in keyof T]: T[K] extends TReadonlyOptional ? K : never }[keyof T] export type ReadonlyPropertyKeys = { [K in keyof T]: T[K] extends TReadonly ? K : never }[keyof T] @@ -208,24 +216,26 @@ export type StaticModifiers = { [K in OptionalPropertyKeys]?: Static } & { [K in RequiredPropertyKeys]: Static } -export type StaticKeyOf = T extends Array ? K : never -export type StaticUnion = { [K in keyof T]: Static }[number] -export type StaticTuple = { [K in keyof T]: Static } -export type StaticObject = ReduceModifiers> -export type StaticDict = { [key: string]: Static } -export type StaticArray = Array> -export type StaticLiteral = T -export type StaticEnum = T +export type StaticKeyOf = T extends Array ? K : never +export type StaticIntersect = UnionToIntersect> +export type StaticUnion = { [K in keyof T]: Static }[number] +export type StaticTuple = { [K in keyof T]: Static } +export type StaticObject = ReduceModifiers> +export type StaticRecord = K extends TString ? { [key: string]: Static } : K extends TNumber ? { [key: number]: Static } : K extends TUnion ? L extends TLiteral[] ? {[K in StaticUnion]: Static } : never : never +export type StaticArray = Array> +export type StaticLiteral = T +export type StaticEnum = T export type StaticConstructor = new (...args: [...{ [K in keyof T]: Static }]) => Static export type StaticFunction = (...args: [...{ [K in keyof T]: Static }]) => Static -export type StaticPromise = Promise> +export type StaticPromise = Promise> export type Static = - T extends TKeyOf ? StaticKeyOf : + T extends TKeyOf ? StaticKeyOf : + T extends TIntersect ? StaticIntersect : T extends TUnion ? StaticUnion : T extends TTuple ? StaticTuple : T extends TObject ? StaticObject : - T extends TDict ? StaticDict : + T extends TRecord ? StaticRecord : T extends TArray ? StaticArray : T extends TEnum ? StaticEnum : T extends TLiteral ? StaticLiteral : @@ -261,12 +271,6 @@ function clone(object: any): any { return object } -function distinct(keys: string[]): string[] { - return Object.keys(keys.reduce((acc, key) => { - return { ...acc, [key]: null } - }, {})) -} - // ------------------------------------------------------------------------ // TypeBuilder // ------------------------------------------------------------------------ @@ -312,14 +316,9 @@ export class TypeBuilder { { ...options, kind: ObjectKind, type: 'object', properties } } - /** `STANDARD` Creates an intersection schema of the given object schemas. */ - public Intersect[]>(items: [...T], options: ObjectOptions = {}): TObject> { - const type = 'object' - const properties = items.reduce((acc, object) => ({ ...acc, ...object['properties'] }), {} as IntersectObjectArray) - const required = distinct(items.reduce((acc, object) => object['required'] ? [ ...acc, ...object['required'] ] : acc, [] as string[])) - return (required.length > 0) - ? { ...options, type, kind: ObjectKind, properties, required } - : { ...options, type, kind: ObjectKind, properties } + /** `STANDARD` Creates an intersection schema. Note this function requires draft `2019-09` to constrain with `unevaluatedProperties`. */ + public Intersect(items: [...T], options: IntersectOptions = {}): TIntersect { + return { ...options, kind: IntersectKind, type: 'object', allOf: items } } /** `STANDARD` Creates a Union schema. */ @@ -327,12 +326,6 @@ export class TypeBuilder { return { ...options, kind: UnionKind, anyOf: items } } - /** `STANDARD` Creates a `{ [key: string]: T }` schema. */ - public Dict(item: T, options: DictOptions = {}): TDict { - const additionalProperties = item - return { ...options, kind: DictKind, type: 'object', additionalProperties } - } - /** `STANDARD` Creates an `Array` schema. */ public Array(items: T, options: ArrayOptions = {}): TArray { return { ...options, kind: ArrayKind, type: 'array', items } @@ -401,6 +394,14 @@ export class TypeBuilder { const keys = Object.keys(schema.properties) as ObjectPropertyKeys[] return {...options, kind: KeyOfKind, type: 'string', enum: keys } } + + /** `STANDARD` Creates a Record schema. */ + public Record(key: K, value: T, options: ObjectOptions = {}): TRecord { + const pattern = key.kind === UnionKind ? `^${key.anyOf.map(key => key.const).join('|')}$` : + key.kind === NumberKind ? '^(0|[1-9][0-9]*)$' : + key.pattern ? key.pattern : '^.*$' + return { ...options, kind: RecordKind, type: 'object', patternProperties: { [pattern]: value } } + } /** `STANDARD` Make all properties in schema object required. */ public Required>(schema: T, options: ObjectOptions = {}): TObject> { @@ -456,7 +457,8 @@ export class TypeBuilder { /** `STANDARD` Omits the `kind` and `modifier` properties from the given schema. */ public Strict(schema: T): T { - return JSON.parse(JSON.stringify(schema)) as T + const $schema = 'https://json-schema.org/draft/2019-09/schema' + return JSON.parse(JSON.stringify({ $schema, ...schema })) as T } /** `EXTENDED` Creates a `constructor` schema. */ @@ -491,13 +493,13 @@ export class TypeBuilder { /** `EXPERIMENTAL` References a schema within a box. */ public Ref, K extends keyof T['definitions']>(box: T, key: K): T['definitions'][K] { - return { $ref: `${box.$id}#/definitions/${key as string}` } as any // facade + return { $ref: `${box.$id}#/definitions/${key as string}` } as unknown as T['definitions'][K] } /** `EXPERIMENTAL` Creates a recursive type. */ public Rec(callback: (self: TAny) => T, $id: string = ''): T { const self = callback({ $ref: `${$id}#/definitions/self` } as any) - return { $id, $ref: `${$id}#/definitions/self`, definitions: { self } } as any as T + return { $id, $ref: `${$id}#/definitions/self`, definitions: { self } } as unknown as T } } diff --git a/tsconfig.json b/tsconfig.json index a65889f..e4fda5a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,10 +4,14 @@ "target": "ESNext", "moduleResolution": "node", "removeComments": true, - "lib": ["ESNext"], + "lib": [ + "ESNext" + ], "baseUrl": ".", "paths": { - "@sinclair/typebox": ["src/typebox.ts"] + "@sinclair/typebox": [ + "src/typebox.ts" + ] } } } \ No newline at end of file