diff --git a/.github/.keep b/.github/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 000000000..7d9b21121 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,23 @@ +name: Build +on: [push, pull_request] +jobs: + TypeBox: + runs-on: ${{ matrix.os }} + strategy: + matrix: + node: [16.x, 18.x, 20.x] + os: [ubuntu-latest, windows-latest, macOS-latest] + steps: + - uses: actions/checkout@v3 + - name: Install Node + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node }} + + - name: Install Packages + run: npm install + + - name: Build Library + run: npm run build + - name: Test Library + run: npm run test \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 4604aa7a8..000000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: GitHub CI - -on: [push, pull_request] - -jobs: - npm: - runs-on: ${{ matrix.os }} - strategy: - matrix: - node-version: [14.x] - os: [ubuntu-latest, windows-latest, macOS-latest] - steps: - - uses: actions/checkout@v2 - - name: Use Node.js - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node-version }} - - name: Install with npm - run: | - npm install - - name: Build - run: | - npm run build - - name: Run tests - run: | - npm run test diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml new file mode 100644 index 000000000..d43b3bc2c --- /dev/null +++ b/.github/workflows/nightly.yml @@ -0,0 +1,30 @@ +name: Build Nightly +on: + schedule: + - cron: '0 18 * * *' # 6pm Daily +jobs: + TypeBox: + runs-on: ${{ matrix.os }} + strategy: + matrix: + node: [20.x] + os: [ubuntu-latest] + steps: + - uses: actions/checkout@v3 + - name: Install Node + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node }} + + - name: Install Packages + run: npm install + + - name: Install TypeScript Latest + run: npm install typescript@latest + - name: Build TypeBox + run: npm run build + + - name: Install TypeScript Next + run: npm install typescript@next + - name: Build TypeBox + run: npm run build \ No newline at end of file diff --git a/.gitignore b/.gitignore index 46029743a..adfbc990e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,3 @@ node_modules target -index.js -spec.js - +dist diff --git a/.vscode/settings.json b/.vscode/settings.json index e4c4348f5..df318270b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,8 @@ { "files.exclude": { "node_modules": true, - "package-lock.json": true, - "target": false - } + "package-lock.json": true + }, + "editor.suggest.showStatusBar": false, + "typescript.tsdk": "node_modules\\typescript\\lib" } \ No newline at end of file diff --git a/changelog.md b/changelog.md deleted file mode 100644 index 1c65a1ae7..000000000 --- a/changelog.md +++ /dev/null @@ -1,237 +0,0 @@ -## [0.23.0](https://www.npmjs.com/package/@sinclair/typebox/v/0.23.0) - -Updates: - -- The types `Type.Namespace()` and `Type.Ref()` are promoted to `Standard`. -- TypeBox adds a new type named `TRef` that is returned on calls to `Type.Ref(...)`. The `TRef` includes a `RefKind` symbol for introspection of the reference type. -- TypeBox now maintains an internal dictionary of all schemas passed that contain an `$id` property. This dictionary is checked whenever a user attempts to reference a type and will throw if attempting to reference a target schema with no $id. -- The types `Type.Partial(...)`, `Type.Required(...)`, `Type.Omit()` and `Type.Pick(...)` now support reference types. Note that when using these functions with references, TypeBox will replicate the source schema and apply the nessasary modifiers to the replication. - -## [0.22.0](https://www.npmjs.com/package/@sinclair/typebox/v/0.22.0) - -Updates: - -- The type `TSchema` is now expressed as an HKT compatible interface. All types now extend the `TSchema` interface and are themselves also expressed as interfaces. This work was undertaken to explore recursive type aliases in future releases. -- The phantom property `_infer` has been renamed to `$static`. Callers should not interact with this property as it will always be `undefined` and used exclusively for optimizing type inference in TypeScript 4.5 and above. -- TypeBox re-adds the feature to deeply introspect schema properties. This feature was temporarily removed on the `0.21.0` update to resolve deep instantiation errors on TypeScript 4.5. -- The `Type.Box(...)` and `Type.Rec(...)` functions internally rename the property `definitions` to `$defs` inline with JSON schema draft 2019-09 conventions. Reference [here](https://opis.io/json-schema/2.x/definitions.html). - -## [0.21.2](https://www.npmjs.com/package/@sinclair/typebox/v/0.21.2) - -Updates: - -- TypeBox now correctly infers for nested union and intersect types. - -Before - -```typescript -const A = Type.Object({ a: Type.String() }) -const B = Type.Object({ b: Type.String() }) -const C = Type.Object({ c: Type.String() }) -const T = Type.Intersect([A, Type.Union([B, C])]) - -// type T = { a: string } & { b: string } & { c: string } -``` -After - -```typescript -const A = Type.Object({ a: Type.String() }) -const B = Type.Object({ b: Type.String() }) -const C = Type.Object({ c: Type.String() }) -const T = Type.Intersect([A, Type.Union([B, C])]) - -// type T = { a: string } & ({ b: string } | { c: string }) -``` - -## [0.21.0](https://www.npmjs.com/package/@sinclair/typebox/v/0.21.0) - -Updates: - -- TypeBox static inference has been updated inline with additional inference constraints added in TypeScript 4.5. All types now include a phantom `_infer` property which contains the inference TS type for a given schema. The type of this property is inferred at the construction of the schema, and referenced directly via `Static`. -- `Type.Box(...)` has been renamed to `Type.Namespace(...)` to draw an analogy with XML's `xmlns` XSD types. - -## [0.20.1](https://www.npmjs.com/package/@sinclair/typebox/v/0.20.1) - -Updates: - -- TypeBox mandates TypeScript compiler version `4.3.5` and above. - -## [0.20.0](https://www.npmjs.com/package/@sinclair/typebox/v/0.20.0) - -Updates: - -- Function `Type.Rec(...)` signature change. -- Minor documentation updates. - -Notes: - -The `Type.Rec(...)` function signature has been changed to allow passing the `$id` as a custom option. This is to align `Type.Rec(...)` with other functions that accept `$id` as an option. `Type.Rec(...)` can work with or without an explicit `$id`, but it is recommend to specify one if the recursive type is nested in an outer schema. - -```typescript -const Node = Type.Rec(Self => Type.Object({ - id: Type.String(), - nodes: Type.Array(Self) -}), { $id: 'Node' }) -``` - -## [0.19.0](https://www.npmjs.com/package/@sinclair/typebox/v/0.19.0) - -Updates: - -- Function `Type.Box(...)` removes `$id` parameter as first argument. -- Function `Type.Ref(...)` is now overloaded to support referencing `Type.Box(...)` and `TSchema`. - -Notes: - -This update changes the signature of `Type.Box(...)` and removes the explicit `$id` passing on the first parameter. The `$id` must be passed as an option if the caller wants to reference that type. - -```typescript -const T = Type.String({ $id: 'T' }) - -const B = Type.Box({ T }, { $id: 'B' }) - -const R1 = Type.Ref(T) // const R1 = { $ref: 'T' } - -const R2 = Type.Ref(B, 'T') // const R2 = { $ref: 'B#/definitions/T' } -``` - -## [0.18.1](https://www.npmjs.com/package/@sinclair/typebox/v/0.18.1) - -- Function `Type.Enum(...)` now expressed with `anyOf`. This to remove the `allowUnionTypes` configuration required to use `enum` with in AJV strict. -- Function `Type.Rec(...)` now takes a required `$id` as the first parameter. -- Function `Type.Strict(...)` no longer includes a `$schema`. Callers can now optionally pass `CustomOptions` on `Type.Strict(...)` - -## [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. - -```typescript -const Operator = Type.Union([ - Type.Literal('+'), - Type.Literal('-'), - Type.Literal('/'), - Type.Literal('*') -]) - -type Expression = Static - -// Defines a self referencing type. -const Expression = Type.Rec(Self => Type.Object({ - left: Type.Union([Self, Type.Number()]), - right: Type.Union([Self, Type.Number()]), - operator: Operator -})) - -function evaluate(expression: Expression): number { - const left = typeof expression.left !== 'number' - ? evaluate(expression.left as Expression) // assert as Expression - : expression.left - const right = typeof expression.right !== 'number' - ? evaluate(expression.right as Expression) // assert as Expression - : expression.right - switch(expression.operator) { - case '+': return left + right - case '-': return left - right - case '*': return left * right - case '/': return left / right - } -} - -const result = evaluate({ - left: { - left: 10, - operator: '*', - right: 4, - }, - operator: '+', - right: 2, -}) // -> 42 -``` - -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. - -```typescript -// Domain objects for the fruit service. -const Fruit = Type.Box('https://fruit.domain.com', { - Apple: Type.Object({ ... }), - Orange: Type.Object({ ... }), -}) - -// An order referencing types of the fruit service. -const Order = Type.Object({ - id: Type.String(), - quantity: Type.Number(), - item: Type.Union([ - Type.Ref(Fruit, 'Apple'), - Type.Ref(Fruit, 'Orange') - ]) -}) -``` -> Note: As of this release, the `Type.Omit()`, `Type.Pick()`, `Type.Partial()`, `Type.Readonly()` and `Type.Intersect()` functions do not work with Reference Types. This may change in later revisions. - -For validation using `Ajv`, its possible to apply the `Box` directly as a schema. - -```typescript -ajv.addSchema(Fruit) // makes all boxed types known to Ajv -``` - -This functionality is flagged as `EXPERIMENTAL` and awaits community feedback. - -## [0.17.1](https://www.npmjs.com/package/@sinclair/typebox/v/0.17.1) - -- Remove default `additionalProperties: false` constraint from all object schemas. - -This update removes the `additionalProperties: false` constraint on all object schemas. This constraint was introduced on `0.16.x` but has resulted in significant downstream problems composing schemas whose types `intersect`. This is due to a JSON schema design principle where constraints should only be added (never removed), and that intersection types may require removal of the `additionalProperties` constraint in some cases, this had resulted in some ambiguity with respect to how TypeBox should handle such intersections. - -This update can also be seen as a precursor towards TypeBox potentially leveraging `unevaluatedProperties` for type intersection in future releases. Implementors should take note that in order to constrain the schema to known properties, one should apply the `additionalProperties: false` as the second argument to `Type.Object({...})`. - -```typescript -const T = Type.Object({ - a: Type.String(), - b: Type.Number() -}, { - additionalProperties: false -}) \ No newline at end of file diff --git a/changelog/0.17.1.md b/changelog/0.17.1.md new file mode 100644 index 000000000..b458c0b15 --- /dev/null +++ b/changelog/0.17.1.md @@ -0,0 +1,15 @@ +## [0.17.1](https://www.npmjs.com/package/@sinclair/typebox/v/0.17.1) + +- Remove default `additionalProperties: false` constraint from all object schemas. + +This update removes the `additionalProperties: false` constraint on all object schemas. This constraint was introduced on `0.16.x` but has resulted in significant downstream problems composing schemas whose types `intersect`. This is due to a JSON schema design principle where constraints should only be added (never removed), and that intersection types may require removal of the `additionalProperties` constraint in some cases, this had resulted in some ambiguity with respect to how TypeBox should handle such intersections. + +This update can also be seen as a precursor towards TypeBox potentially leveraging `unevaluatedProperties` for type intersection in future releases. Implementers should take note that in order to constrain the schema to known properties, one should apply the `additionalProperties: false` as the second argument to `Type.Object({...})`. + +```typescript +const T = Type.Object({ + a: Type.String(), + b: Type.Number() +}, { + additionalProperties: false +}) \ No newline at end of file diff --git a/changelog/0.17.4.md b/changelog/0.17.4.md new file mode 100644 index 000000000..07a8cf5e4 --- /dev/null +++ b/changelog/0.17.4.md @@ -0,0 +1,38 @@ +## [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` namespaces. + +The `Type.Ref()` function is limited to referencing from boxes only. The following is an example. + +```typescript +// Domain objects for the fruit service. +const Fruit = Type.Box('https://fruit.domain.com', { + Apple: Type.Object({ ... }), + Orange: Type.Object({ ... }), +}) + +// An order referencing types of the fruit service. +const Order = Type.Object({ + id: Type.String(), + quantity: Type.Number(), + item: Type.Union([ + Type.Ref(Fruit, 'Apple'), + Type.Ref(Fruit, 'Orange') + ]) +}) +``` +> Note: As of this release, the `Type.Omit()`, `Type.Pick()`, `Type.Partial()`, `Type.Readonly()` and `Type.Intersect()` functions do not work with Reference Types. This may change in later revisions. + +For validation using `Ajv`, its possible to apply the `Box` directly as a schema. + +```typescript +ajv.addSchema(Fruit) // makes all boxed types known to Ajv +``` + +This functionality is flagged as `EXPERIMENTAL` and awaits community feedback. \ No newline at end of file diff --git a/changelog/0.17.6.md b/changelog/0.17.6.md new file mode 100644 index 000000000..291ea5f3e --- /dev/null +++ b/changelog/0.17.6.md @@ -0,0 +1,56 @@ +## [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 operands. + +```typescript +const Operator = Type.Union([ + Type.Literal('+'), + Type.Literal('-'), + Type.Literal('/'), + Type.Literal('*') +]) + +type Expression = Static + +// Defines a self referencing type. +const Expression = Type.Rec(Self => Type.Object({ + left: Type.Union([Self, Type.Number()]), + right: Type.Union([Self, Type.Number()]), + operator: Operator +})) + +function evaluate(expression: Expression): number { + const left = typeof expression.left !== 'number' + ? evaluate(expression.left as Expression) // assert as Expression + : expression.left + const right = typeof expression.right !== 'number' + ? evaluate(expression.right as Expression) // assert as Expression + : expression.right + switch(expression.operator) { + case '+': return left + right + case '-': return left - right + case '*': return left * right + case '/': return left / right + } +} + +const result = evaluate({ + left: { + left: 10, + operator: '*', + right: 4, + }, + operator: '+', + right: 2, +}) // -> 42 +``` + +This functionality is flagged as `EXPERIMENTAL` and awaits community feedback. \ No newline at end of file diff --git a/changelog/0.18.0.md b/changelog/0.18.0.md new file mode 100644 index 000000000..88a7d4f2e --- /dev/null +++ b/changelog/0.18.0.md @@ -0,0 +1,22 @@ +## [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. \ No newline at end of file diff --git a/changelog/0.18.1.md b/changelog/0.18.1.md new file mode 100644 index 000000000..1a0bbe7f9 --- /dev/null +++ b/changelog/0.18.1.md @@ -0,0 +1,5 @@ +## [0.18.1](https://www.npmjs.com/package/@sinclair/typebox/v/0.18.1) + +- Function `Type.Enum(...)` now expressed with `anyOf`. This to remove the `allowUnionTypes` configuration required to use `enum` with in AJV strict. +- Function `Type.Rec(...)` now takes a required `$id` as the first parameter. +- Function `Type.Strict(...)` no longer includes a `$schema`. Callers can now optionally pass `CustomOptions` on `Type.Strict(...)` \ No newline at end of file diff --git a/changelog/0.19.0.md b/changelog/0.19.0.md new file mode 100644 index 000000000..377ccdbff --- /dev/null +++ b/changelog/0.19.0.md @@ -0,0 +1,20 @@ +## [0.19.0](https://www.npmjs.com/package/@sinclair/typebox/v/0.19.0) + +Updates: + +- Function `Type.Box(...)` removes `$id` parameter as first argument. +- Function `Type.Ref(...)` is now overloaded to support referencing `Type.Box(...)` and `TSchema`. + +Notes: + +This update changes the signature of `Type.Box(...)` and removes the explicit `$id` passing on the first parameter. The `$id` must be passed as an option if the caller wants to reference that type. + +```typescript +const T = Type.String({ $id: 'T' }) + +const B = Type.Box({ T }, { $id: 'B' }) + +const R1 = Type.Ref(T) // const R1 = { $ref: 'T' } + +const R2 = Type.Ref(B, 'T') // const R2 = { $ref: 'B#/definitions/T' } +``` \ No newline at end of file diff --git a/changelog/0.20.0.md b/changelog/0.20.0.md new file mode 100644 index 000000000..80a01da97 --- /dev/null +++ b/changelog/0.20.0.md @@ -0,0 +1,17 @@ +## [0.20.0](https://www.npmjs.com/package/@sinclair/typebox/v/0.20.0) + +Updates: + +- Function `Type.Rec(...)` signature change. +- Minor documentation updates. + +Notes: + +The `Type.Rec(...)` function signature has been changed to allow passing the `$id` as a custom option. This is to align `Type.Rec(...)` with other functions that accept `$id` as an option. `Type.Rec(...)` can work with or without an explicit `$id`, but it is recommend to specify one if the recursive type is nested in an outer schema. + +```typescript +const Node = Type.Rec(Self => Type.Object({ + id: Type.String(), + nodes: Type.Array(Self) +}), { $id: 'Node' }) +``` \ No newline at end of file diff --git a/changelog/0.20.1.md b/changelog/0.20.1.md new file mode 100644 index 000000000..2dfec120f --- /dev/null +++ b/changelog/0.20.1.md @@ -0,0 +1,5 @@ +## [0.20.1](https://www.npmjs.com/package/@sinclair/typebox/v/0.20.1) + +Updates: + +- TypeBox mandates TypeScript compiler version `4.3.5` and above. \ No newline at end of file diff --git a/changelog/0.21.0.md b/changelog/0.21.0.md new file mode 100644 index 000000000..31df5f78f --- /dev/null +++ b/changelog/0.21.0.md @@ -0,0 +1,7 @@ +## [0.21.0](https://www.npmjs.com/package/@sinclair/typebox/v/0.21.0) + +Updates: + +- TypeBox static inference has been updated inline with additional inference constraints added in TypeScript 4.5. All types now include a phantom `_infer` property which contains the inference TS type for a given schema. The type of this property is inferred at the construction of the schema, and referenced directly via `Static`. +- `Type.Box(...)` has been renamed to `Type.Namespace(...)` to draw an analogy with XML's `xmlns` XSD types. + diff --git a/changelog/0.21.2.md b/changelog/0.21.2.md new file mode 100644 index 000000000..12d31df53 --- /dev/null +++ b/changelog/0.21.2.md @@ -0,0 +1,26 @@ +## [0.21.2](https://www.npmjs.com/package/@sinclair/typebox/v/0.21.2) + +Updates: + +- TypeBox now correctly infers for nested union and intersect types. + +Before + +```typescript +const A = Type.Object({ a: Type.String() }) +const B = Type.Object({ b: Type.String() }) +const C = Type.Object({ c: Type.String() }) +const T = Type.Intersect([A, Type.Union([B, C])]) + +// type T = { a: string } & { b: string } & { c: string } +``` +After + +```typescript +const A = Type.Object({ a: Type.String() }) +const B = Type.Object({ b: Type.String() }) +const C = Type.Object({ c: Type.String() }) +const T = Type.Intersect([A, Type.Union([B, C])]) + +// type T = { a: string } & ({ b: string } | { c: string }) +``` \ No newline at end of file diff --git a/changelog/0.22.0.md b/changelog/0.22.0.md new file mode 100644 index 000000000..f65eeeb7a --- /dev/null +++ b/changelog/0.22.0.md @@ -0,0 +1,8 @@ +## [0.22.0](https://www.npmjs.com/package/@sinclair/typebox/v/0.22.0) + +Updates: + +- The type `TSchema` is now expressed as an HKT compatible interface. All types now extend the `TSchema` interface and are themselves also expressed as interfaces. This work was undertaken to explore recursive type aliases in future releases. +- The phantom property `_infer` has been renamed to `$static`. Callers should not interact with this property as it will always be `undefined` and used exclusively for optimizing type inference in TypeScript 4.5 and above. +- TypeBox re-adds the feature to deeply introspect schema properties. This feature was temporarily removed on the `0.21.0` update to resolve deep instantiation errors on TypeScript 4.5. +- The `Type.Box(...)` and `Type.Rec(...)` functions internally rename the property `definitions` to `$defs` inline with JSON schema draft 2019-09 conventions. Reference [here](https://opis.io/json-schema/2.x/definitions.html). \ No newline at end of file diff --git a/changelog/0.23.0.md b/changelog/0.23.0.md new file mode 100644 index 000000000..5dce7d99f --- /dev/null +++ b/changelog/0.23.0.md @@ -0,0 +1,8 @@ +## [0.23.0](https://www.npmjs.com/package/@sinclair/typebox/v/0.23.0) + +Updates: + +- The types `Type.Namespace(...)` and `Type.Ref(...)` are promoted to `Standard`. +- TypeBox now includes an additional type named `TRef<...>` that is returned on calls to `Type.Ref(...)`. The `TRef<...>` includes a new `RefKind` symbol for introspection of the reference type. +- TypeBox now maintains an internal dictionary of all schemas passed that contain an `$id` property. This dictionary is checked whenever a user attempts to reference a type and will throw if attempting to reference a target schema with no `$id`. +- The types `Type.Partial(...)`, `Type.Required(...)`, `Type.Omit()` and `Type.Pick(...)` now support reference types. Note that when using these functions with references, TypeBox will replicate the source schema and apply the necessary modifiers to the replication. \ No newline at end of file diff --git a/changelog/0.23.1.md b/changelog/0.23.1.md new file mode 100644 index 000000000..565f779ba --- /dev/null +++ b/changelog/0.23.1.md @@ -0,0 +1,5 @@ +## [0.23.1](https://www.npmjs.com/package/@sinclair/typebox/v/0.23.1) + +Updates: + +- The `Type.KeyOf(...)` type can now accept references of `Type.Ref(TObject)` \ No newline at end of file diff --git a/changelog/0.23.3.md b/changelog/0.23.3.md new file mode 100644 index 000000000..bb89450bc --- /dev/null +++ b/changelog/0.23.3.md @@ -0,0 +1,5 @@ +## [0.23.3](https://www.npmjs.com/package/@sinclair/typebox/v/0.23.3) + +Updates: + +- Fix: Rename BoxKind to NamespaceKind \ No newline at end of file diff --git a/changelog/0.24.0.md b/changelog/0.24.0.md new file mode 100644 index 000000000..b433b5827 --- /dev/null +++ b/changelog/0.24.0.md @@ -0,0 +1,28 @@ +## [0.24.0](https://www.npmjs.com/package/@sinclair/typebox/v/0.24.0) + +Changes: + +- The `kind` and `modifier` keywords are now expressed as symbol keys. This change allows AJV to leverage TypeBox schemas directly without explicit configuration of `kind` and `modifier` in strict mode. +- `Type.Intersect([...])` now returns a composite `TObject` instead of a `allOf` schema representation. This change allows intersected types to be leveraged in calls to `Omit`, `Pick`, `Partial`, `Required`. +- `Type.Void(...)` now generates a `{ type: null }` schema representation. This is principally used for RPC implementations where a RPC target function needs to respond with a serializable value for `void` return. +- `Type.Rec(...)` renamed to `Type.Recursive(...)` and now supports non-mutual recursive type inference. + +Added: + +- `Type.Unsafe(...)`. This type enables custom schema representations whose static type is informed by generic type T. +- `Type.Uint8Array(...)`. This is a non-standard schema that can be configured on AJV to enable binary buffer range validation. +- Added optional extended `design` property on all schema options. This property can be used to specify design time metadata when rendering forms. + +Compiler: + +- TypeBox now provides an optional experimental type compiler that can be used to validate types without AJV. This compiler is not a standard JSON schema compiler and will only compile TypeBox's known schema representations. For full JSON schema validation, AJV should still be the preference. This compiler is a work in progress. + +Value: + +- TypeBox now provides a value generator that can generate default values from TypeBox types. + +Breaking Changes: + +- `Type.Intersect(...)` is constrained to accept types of `TObject` only. +- `Type.Namespace(...)` has been removed. +- The types `TUnion`, `TEnum`, `KeyOf` and `TLiteral[]` are all now expressed via `allOf`. For Open API users, Please consider `Type.Unsafe()` to express `enum` string union representations. Documentation on using `Type.Unsafe()` can be found [here](https://github.com/sinclairzx81/typebox#Unsafe-Types) \ No newline at end of file diff --git a/changelog/0.24.15.md b/changelog/0.24.15.md new file mode 100644 index 000000000..69745b875 --- /dev/null +++ b/changelog/0.24.15.md @@ -0,0 +1,10 @@ +## [0.24.15](https://www.npmjs.com/package/@sinclair/typebox/v/0.24.15) + +Added: +- `Conditional.Extends(...)` This enables TypeBox to conditionally map types inline with TypeScripts structural equivalence checks. Tested against TypeScript 4.7.4. +- `Conditional.Extract(...)` Which analogs TypeScripts `Extract<...>` utility type. Additional information [here](https://www.typescriptlang.org/docs/handbook/utility-types.html#extracttype-union) +- `Conditional.Exclude(...)` Which analogs TypeScripts `Exclude<...>` utility type. Additional information [here](https://www.typescriptlang.org/docs/handbook/utility-types.html#excludeuniontype-excludedmembers) +- `Type.Parameters(...)` Returns the parameters of a `TFunction` as a `TTuple` +- `Type.ReturnType(...)` Returns the return type schema of a `TFunction` +- `Type.ConstructorParameters(...)` Returns the parameters of a `TConstructor` as a `TTuple` +- `Type.InstanceType(...)` Returns the instance type schema of a `TConstructor` \ No newline at end of file diff --git a/changelog/0.24.44.md b/changelog/0.24.44.md new file mode 100644 index 000000000..e5aac8bde --- /dev/null +++ b/changelog/0.24.44.md @@ -0,0 +1,18 @@ +## [0.24.44](https://www.npmjs.com/package/@sinclair/typebox/v/0.24.44) + +Updates: +- [189](https://github.com/sinclairzx81/typebox/pull/189) Both `Value.Error(T, value)` and `TypeCheck.Error(value)` now return an iterator for validation errors. +- [191](https://github.com/sinclairzx81/typebox/pull/191) TypeBox now provides a `TypeGuard` API that can be used to check the structural validity of TypeBox type. The TypeGuard can be used in reflection / code generation scenarios to resolve the appropriate inner `TSchema` type while traversing a outer type. +- [197](https://github.com/sinclairzx81/typebox/pull/197) TypeBox now implements conditional runtime type mapping. This functionality is offered as separate import for the `0.24.0` release but may be provided as standard type in later releases. This API enables `type T = Foo extends Bar ? true : false` conditional checks to be implemented at runtime. This API also provides the `Exclude` and `Extract` utility types which are implemented through conditional types in TypeScript. +- [199](https://github.com/sinclairzx81/typebox/pull/199) TypeBox now provides better support for variadic function and constructor signatures. Currently variadic types are mapped as `Tuple` types. +- [200](https://github.com/sinclairzx81/typebox/pull/200) The types `TPick` and `TOmit` now support types of `TUnion[]>` to be used to select properties. Additionally, `KeyOf` now returns `TUnion[]>`, allowing `KeyOf` schemas to be passed to `TPick` and `TOmit`. +- [214](https://github.com/sinclairzx81/typebox/pull/214) TypeBox now provides better support for i18n. To achieve this, TypeBox includes fixed mappable error codes on the `ValueError` type. These codes can be used by external implementers to create localized error messages. TypeBox may include localized error codes as an optional import in future releases. +- [288](https://github.com/sinclairzx81/typebox/pull/228) TypeBox now allows users to implement custom string validator formats. These formats are internally shared between the `Value` and `TypeCompiler` API's. TypeBox does not currently provide any built in formats, however the standard expected set (email, uuid, uri, etc) may be provided via optional import (inline with ajv-formats usage) +- [229](https://github.com/sinclairzx81/typebox/pull/229) The `Value.Cast()` function now implements automatic coercion of string, number and Boolean types. +- [231](https://github.com/sinclairzx81/typebox/pull/231) TypeBox provides a new `Value.Diff()` and `Value.Patch()` utility API for JavaScript values. This API is intended to provide a basis for the efficient transmission of state updates across a network. This API can diff any JavaScript value (typed or untyped) but is recommended to be used in conjunction with a formal static type. +- [236](https://github.com/sinclairzx81/typebox/pull/236) TypeBox now implements the `TNever` type. This type is analogous to TypeScript's `never` type and is used in instances a composition results in a non-reconcilable type. Currently this type is implemented for empty `TUnion<[]>` types only. Future releases may utilize this type for planned updates to `TIntersect` (for example `string & number` resolves to `never`) +- [241](https://github.com/sinclairzx81/typebox/pull/241) [247](https://github.com/sinclairzx81/typebox/pull/247) TypeBox now exposes a ValuePointer API that can be used to mutate a value via an RFC6901 JSON Pointer. Previously this functionality was internally used by `Value.Diff()` and `Value.Patch()` functions but is now offered as an optional import for implementations that need to update values manually through pointer references. + +Additional: + +- This project now includes two reference code generation utilities that can be used in custom build tooling. The first is `TypeScriptCodeGen` which will remap TypeScript `interface` and `type` definitions to TypeBox types. The second is `TypeBoxCodeGen` which will map existing TypeBox types into TypeScript type definitions. These implementations are not expected to be part of the TypeBox package, but users are free to clone and enhance them in their existing tool chains. Reference implementations can be found https://github.com/sinclairzx81/typebox/tree/master/codegen \ No newline at end of file diff --git a/changelog/0.24.49.md b/changelog/0.24.49.md new file mode 100644 index 000000000..e0fd0a65e --- /dev/null +++ b/changelog/0.24.49.md @@ -0,0 +1,9 @@ +## [0.24.49](https://www.npmjs.com/package/@sinclair/typebox/v/0.24.49) + +Updates: + +- [264](https://github.com/sinclairzx81/typebox/pull/264) TypeBox now provides preliminary support for non Boolean `additionalProperties`. This allows existing `TObject` schemas to be augmented with additional properties of a known type. + +Additional: + +- TypeBox provides an additional reference `codegen` module for generating raw JSON Schema from TypeScript types via the TS compiler API. This generator may be used in future tooling. \ No newline at end of file diff --git a/changelog/0.24.6.md b/changelog/0.24.6.md new file mode 100644 index 000000000..b329dec8f --- /dev/null +++ b/changelog/0.24.6.md @@ -0,0 +1,20 @@ +## [0.24.6](https://www.npmjs.com/package/@sinclair/typebox/v/0.24.6) + +Added: + +- TypeBox now offers a `TypeGuard` module for structurally checking TypeBox schematics. This module can be used in runtime type reflection scenarios where it's helpful to test a schema is of a particular form. This module can be imported under the `@sinclair/typebox/guard` import path. + +Example: + +```typescript +import { TypeGuard } from '@sinclair/typebox/guard' + +const T: any = {} // T is any + +const { type } = T // unsafe: type is any + +if(TypeGuard.IsString(T)) { + + const { type } = T // safe: type is 'string' +} +``` \ No newline at end of file diff --git a/changelog/0.24.8.md b/changelog/0.24.8.md new file mode 100644 index 000000000..05f1576d0 --- /dev/null +++ b/changelog/0.24.8.md @@ -0,0 +1,6 @@ +## [0.24.8](https://www.npmjs.com/package/@sinclair/typebox/v/0.24.8) + +Added: +- `Value.Cast(T, value)` structurally casts a value into another form while retaining information within the original value. +- `Value.Check(T, value)` provides slow dynamic type checking for values. For performance, one should consider the `TypeCompiler` or `Ajv` validator. +- `Value.Errors(T, value)` returns an iterator of errors found in a given value. \ No newline at end of file diff --git a/changelog/0.25.0.md b/changelog/0.25.0.md new file mode 100644 index 000000000..5085679b5 --- /dev/null +++ b/changelog/0.25.0.md @@ -0,0 +1,7 @@ +## [0.25.0](https://www.npmjs.com/package/@sinclair/typebox/v/0.25.0) + +Updates: + +- [271](https://github.com/sinclairzx81/typebox/pull/271) Adds a new non-standard `Type.Date()` type. This type joins the existing `Type.UInt8Array()` as a promoted extended type used to represent core JavaScript primitives. It's inclusion was prompted by end user requirements to validate Date objects prior to writing them to Date supported API's and where serialization of the Date object is handled internally by the API. + +- [271](https://github.com/sinclairzx81/typebox/pull/271) Redesign of Extended Type representations. Extended types been updated to provide external validators (such as Ajv) additional standard proporties to use when defining the custom schema. These properties are `instanceOf` (used for validating a class `object` instances), and `typeOf` (when validating `value` types). Information on configuring AJV for these properties can be found in the AJV section of the TypeBox readme. \ No newline at end of file diff --git a/changelog/0.25.10.md b/changelog/0.25.10.md new file mode 100644 index 000000000..fc1d3d567 --- /dev/null +++ b/changelog/0.25.10.md @@ -0,0 +1,5 @@ +## [0.25.10](https://www.npmjs.com/package/@sinclair/typebox/v/0.25.10) + +Updates: + +- [283](https://github.com/sinclairzx81/typebox/pull/283) Updates the custom type validator callback signature to accept a schema instance. The schema instance may include additional constraints (such as options) that may be used during the validation process. `Custom.Set('', (schema, value) => { ... })`. \ No newline at end of file diff --git a/changelog/0.25.11.md b/changelog/0.25.11.md new file mode 100644 index 000000000..ad5144be0 --- /dev/null +++ b/changelog/0.25.11.md @@ -0,0 +1,5 @@ +## [0.25.11](https://www.npmjs.com/package/@sinclair/typebox/v/0.25.11) + +Updates: + +- [286](https://github.com/sinclairzx81/typebox/pull/286) implements a FNV1A-64 non cryptographic hashing function in TypeBox. This function should not be used in place of cryptographic hashing functions, rather it's purpose is to provide relatively fast, hashing mechanism to assist with checks for arrays with uniqueItems constraints, specifically for cases where the array may contains reference values (such as objects, arrays, Dates and Uint8Array). This function is provided via `Value.Hash()` for convenience as the hash may be useful to generate a numeric identifier for values (with some considerations to React array rendering in absence of key or identifier) \ No newline at end of file diff --git a/changelog/0.25.18.md b/changelog/0.25.18.md new file mode 100644 index 000000000..d762e176f --- /dev/null +++ b/changelog/0.25.18.md @@ -0,0 +1,5 @@ +## [0.25.18](https://www.npmjs.com/package/@sinclair/typebox/v/0.25.18) + +Updates: + +- [307](https://github.com/sinclairzx81/typebox/pull/307) implements date conversion when casting values with `Value.Cast(Type.Date(), ...)`. Castable values include numbers (interpretted as timestamps) and iso8601 string values. UNCASTABLE values will result in dates with values of `1970-01-01T00:00:00.000Z`. This version also includes more robust checks for Dates initialized with invalid values. \ No newline at end of file diff --git a/changelog/0.25.22.md b/changelog/0.25.22.md new file mode 100644 index 000000000..3e9240379 --- /dev/null +++ b/changelog/0.25.22.md @@ -0,0 +1,5 @@ +## [0.25.22](https://www.npmjs.com/package/@sinclair/typebox/v/0.25.22) + +Updates: + +- [323](https://github.com/sinclairzx81/typebox/pull/323) adds compiler support for UTF-16 (unicode) characters for schema identifiers. \ No newline at end of file diff --git a/changelog/0.25.23.md b/changelog/0.25.23.md new file mode 100644 index 000000000..76e93040a --- /dev/null +++ b/changelog/0.25.23.md @@ -0,0 +1,10 @@ +## [0.25.23](https://www.npmjs.com/package/@sinclair/typebox/v/0.25.23) + +Updates: + +- [324](https://github.com/sinclairzx81/typebox/pull/324) TypeScript Language Service now presents JSDoc comments when inferring static object properties. (IntelliSense) +- [325](https://github.com/sinclairzx81/typebox/pull/325) Additional property inference optimizations. + +Additional: + +- Huge thank you to GITHUB user [stevezhu](https://github.com/stevezhu) for these excellent contributions. \ No newline at end of file diff --git a/changelog/0.25.24.md b/changelog/0.25.24.md new file mode 100644 index 000000000..f45dc03c0 --- /dev/null +++ b/changelog/0.25.24.md @@ -0,0 +1,10 @@ +## [0.25.24](https://www.npmjs.com/package/@sinclair/typebox/v/0.25.23) + +Updates: + +- [331](https://github.com/sinclairzx81/typebox/pull/331) Implements an additional check specific to property types of `required & undefined`. This to ensure the property key exists when the property value extends `undefined`. +- [331](https://github.com/sinclairzx81/typebox/pull/331) Documentation updates for AJV and TypeCompiler + +Additional: + +- [331](https://github.com/sinclairzx81/typebox/pull/331) Remove unused recursive code paths for create and cast. \ No newline at end of file diff --git a/changelog/0.25.9.md b/changelog/0.25.9.md new file mode 100644 index 000000000..c0155a84b --- /dev/null +++ b/changelog/0.25.9.md @@ -0,0 +1,5 @@ +## [0.25.9](https://www.npmjs.com/package/@sinclair/typebox/v/0.25.9) + +Updates: + +- [282](https://github.com/sinclairzx81/typebox/pull/282) TypeBox now supports custom types. These types require the user to specify a custom `[Kind]` string on the type. Custom types can be registered via `Custom.Set('', (value) => { ... })` which allow the TypeCompiler and Value API's to make use of user defined validation logic. \ No newline at end of file diff --git a/changelog/0.26.0.md b/changelog/0.26.0.md new file mode 100644 index 000000000..34b69e876 --- /dev/null +++ b/changelog/0.26.0.md @@ -0,0 +1,394 @@ +## [0.26.0](https://www.npmjs.com/package/@sinclair/typebox/v/0.26.0) + +## Overview + +TypeBox now provides "runtime conditional types" (formally the `Conditional` module) as standard on `Type.*`. Additional updates in this revision include automatic union and intersection unwrap, universal support for utility types, several ergonomic enhancements and additional options for framework integrators. This revision also carries out a number an internal refactorings to reduce the amount of submodule imports. + +Revision 0.26.0 is a milestone release for the TypeBox project and requires a minor semver update. + +## Contents + +- Enhancements + - [Standard Type Builder](#Standard-Type-Builder) + - [Automatic Unwrap for Union and Intersect](#Automatic-Unwrap-for-Union-and-Intersect) + - [Intersect and Union now Compose](#Intersect-and-Union-now-Compose) + - [Runtime Conditional Types](#Runtime-Conditional-Types) + - [Value Convert](#Value-Convert) + - [Error Iterator](#Error-Iterator) + - [Codegen without JIT](#Codegen-without-JIT) + - [Standard Type (Composite)](#Standard-Type-Composite) + - [Standard Type (Not)](#Standard-Type-Not) + - [Extended Type (BigInt)](#Extended-Type-BigInt) + - [Extended Type (Symbol)](#Extended-Type-Symbol) +- Breaking + - [Minimum TypeScript Version](#Minimum-TypeScript-Version) + - [Intersect Schema Representation](#Intersect-Schema-Representation) + - [Never Schema Representation](#Never-Schema-Representation) + - [Value Cast and Convert](#Value-Cast-and-Convert) + - [Moved TypeGuard Module](#Moved-TypeGuard-Module) + - [Format Renamed to FormatRegistry](#Format-Renamed-to-FormatRegistry) + - [Custom Renamed to TypeRegistry](#Custom-Renamed-to-TypeRegistry) + + + +## Standard Type Builder + +Revision 0.26.0 exports a new type builder called `StandardType`. This builder only allows for the construction JSON Schema compliant types by omitting all Extended types. + +```typescript +import { StandardType as Type, Static } from '@sinclair/typebox' + +const T = Type.Date() // error: no such function +``` + + + +## Automatic Unwrap for Union and Intersect + +Revision 0.26.0 will automatically unwrap unions and intersections for the following cases. + +```typescript +const T1 = Type.Union([Type.String(), Type.Number()]) // TUnion<[TString, TNumber]> + +const T2 = Type.Union([Type.String()]) // TString + +const T3 = Type.Union([]) // TNever +``` + + + +## Intersect and Union now Compose + +Revision 0.26.0 re-enables support for union and intersection type composition. These types are also made compatible with `Pick`, `Omit`, `Partial`, `Required` and `KeyOf` utility types. + +```typescript +const A = Type.Object({ type: Type.Literal('A') }) +const B = Type.Object({ type: Type.Literal('B') }) +const C = Type.Object({ type: Type.Literal('C') }) + +const Union = Type.Union([A, B, C]) + +const Extended = Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number() +}) + +const T = Type.Intersect([Union, Extended]) // type T = ({ + // type: "A"; + // } | { + // type: "B"; + // } | { + // type: "C"; + // }) & { + // x: number; + // y: number; + // z: number; + // } + +const K = Type.KeyOf(T) // type K = "type" | "x" | "y" | "z" + +const P = Type.Pick(T, ['type', 'x']) // type P = ({ + // type: "A"; + // } | { + // type: "B"; + // } | { + // type: "C"; + // }) & { + // x: number; + // } + +const O = Type.Partial(P) // type O = ({ + // type?: "A" | undefined; + // } | { + // type?: "B" | undefined; + // } | { + // type?: "C" | undefined; + // }) & { + // x?: number | undefined; + // } +``` + + + +## Runtime Conditional Types + +Revision 0.26.0 adds the runtime conditional types `Extends`, `Extract` and `Exclude` as standard. + +#### TypeScript + +```typescript +type T0 = string extends number ? true : false +// ^ false +type T1 = Extract +// ^ number +type T2 = Exclude +// ^ string +``` + +#### TypeBox +```typescript +const T0 = Type.Extends(Type.String(), Type.Number(), Type.Literal(true), Type.Literal(false)) +// ^ TLiteral +const T1 = Type.Extract(Type.Union([Type.String(), Type.Number()]), Type.Number()) +// ^ TNumber +const T2 = Type.Exclude(Type.Union([Type.String(), Type.Number()]), Type.Number()) +// ^ TString +``` + + + +## Value Convert + +Revision 0.26.0 adds a new `Convert` function to the `Value.*` module. This function will perform a type coercion for any value mismatched to its type if a reasonable conversion is possible. + +```typescript +const T = Type.Number() + +const A = Value.Convert(T, '42') // const A: unknown = 42 - ... Convert(...) will return `unknown` + +const B = Value.Check(T, A) // const B = true - ... so should be checked +``` + + + +## Error Iterator + +Revision 0.26.0 now returns a `ValueErrorIterator` for `.Errors(...)`. This iterator provides a utility function to obtain the first error only. To obtain all errors, continue to use `for-of` enumeration or array spread syntax. + +```typescript +const T = Type.Number() + +const First = Value.Errors(T, 'foo').First() // const First = { path: '', message: 'Expected number', ... } + +const All = [...Value.Errors(T, 'foo')] // const All = [{ path: '', message: 'Expected number', ... }] +``` + + + + +## Codegen without JIT + +Revision 0.26.0 adds a `.Code()` function to the `TypeCompiler` to enable code generation without JIT evaluation. + +```typescript +import { TypeCompiler } from '@sinclair/typebox/compiler' + +const T = Type.Object({ + x: Type.Number(), + y: Type.Number() +}) + +const C = TypeCompiler.Code(T) // return function check(value) { + // return ( + // (typeof value === 'object' && value !== null) && + // !Array.isArray(value) && + // typeof value.x === 'number' && + // Number.isFinite(value.x) && + // typeof value.y === 'number' && + // Number.isFinite(value.y) + // ) + // } +``` + + + +## Standard Type (Not) + +Revision 0.26.0 introduces the `Not` standard type. This type allows for the inversion of assertion logic which can be useful to narrow for broader types. + +#### Example 1 + +```typescript +const T = Type.Not(Type.String({ pattern: 'A|B|C' }), Type.String()) + +Value.Check(T, 'A') // false +Value.Check(T, 'B') // false +Value.Check(T, 'C') // false +Value.Check(T, 'D') // true +``` + +#### Example 2 + +```typescript +const Even = Type.Number({ multipleOf: 2 }) +const Odd = Type.Not(Even, Type.Number()) + +Value.Check(Even, 0) // true +Value.Check(Even, 1) // false +Value.Check(Even, 2) // true + +Value.Check(Odd, 0) // false +Value.Check(Odd, 1) // true +Value.Check(Odd, 2) // false +``` + + + +## Standard Type (Composite) + +Revision 0.26.0 includes a new `Composite` standard type. This type will combine an array of `TObject[]` into a `TObject` by taking a union of any overlapping properties. + +```typescript +const A = Type.Object({ type: Type.Literal('A') }) + +const B = Type.Object({ type: Type.Literal('B') }) + +const C = Type.Object({ type: Type.Literal('C'), value: Type.Number() }) + +const T = Type.Composite([A, B, C]) // type T = { + // type: 'A' | 'B' | 'C' + // value: number + // } +``` + + + +## Extended Type (Symbol) + +Revision 0.26.0 provides provisional support for `Symbol` type validation. + +```typescript +const T = Type.Symbol() + +Value.Check(A, Symbol('Foo')) // true +``` + + + +## Extended Type (BigInt) + +Revision 0.26.0 provides provisional support for `BigInt` type validation. + +```typescript +const T = Type.BigInt({ minimum: 10n }) + +Value.Check(B, 1_000_000n) // true +``` + + + +## Breaking Changes + +The following are breaking changed in Revision 0.26.0 + + + +## Minimum TypeScript Version + +Revision 0.26.0 requires a minimum recommended TypeScript version of `4.2.3`. Version `4.1.5` is no longer supported. + + + +## Intersect Schema Representation + +Revision 0.26.0 changes the schema representation for `Intersect`. Revision 0.25.0 would construct a composite `object` type, in 0.26.0, `Intersect` is expressed as `anyOf`. If upgrading, consider using `Type.Composite(...)` to return backwards compatible representations. + +#### Intersect 0.25.0 + +```typescript +const T = Type.Intersect([ // const U = { + Type.Object({ // type: 'object', + x: Type.Number(), // required: ['x', 'y'], + }), // properties: { + Type.Object({ // x: { + y: Type.Number(), // type: 'number' + }) // }, +]) // y: { + // type: 'number' + // } + // } + // } +``` +#### Intersect 0.26.0 + +```typescript +const T = Type.Intersect([ // const U = { + Type.Object({ // type: 'object', + x: Type.Number(), // allOf: [{ + }), // type: 'object', + Type.Object({ // required: [ 'x' ], + y: Type.Number(), // properties: { + }) // x: { type: 'number' } +]) // } + // }, { + // type: 'object', + // required: ['y'], + // properties: { + // y: { type: 'number' } + // } + // }] + // } +``` + + + +## Never Schema Representation + +Revision 0.26.0 simplifies the representation for `TNever`. Previous versions of TypeBox used an illogical intersection of Boolean constants via `allOf`. In 0.26.0, `never` is expressed as a `not` schema of type `any`. + +#### Intersect 0.25.0 + +```typescript +const T = Type.Never() // const T = { + // allOf: [ + // { type: 'boolean', const: true } + // { type: 'boolean', const: false } + // ] + // } +``` +#### Intersect 0.26.0 + +```typescript +const T = Type.Never() // const T = { not: {} } +``` + + + +## Value Cast and Convert + +Revision 0.26.0 removes the `Cast` functions ability to coerce values. Use the new `Convert` function prior to `Cast`. + +```typescript +const T = Type.Number() + +const V = Value.Cast(T, '42') // const V = 42 - 0.25.0 coerces to 42 + +const V = Value.Cast(T, Value.Convert(T, '42')) // const V = 42 - 0.26.0 convert then cast +``` + + + +## Moved TypeGuard Module + +The `TypeGuard` is now imported via the `@sinclair/typebox` module. This move is due to the TypeBox compositor internally using the guard when constructing types. + +```typescript +import { TypeGuard } from '@sinclair/typebox/guard' // 0.25.0 + +import { TypeGuard } from '@sinclair/typebox' // 0.26.0 +``` + + + +## Format Renamed to FormatRegistry + +The `Format` module has been renamed to `FormatRegistry` and moved to the `typebox.ts` module. + +```typescript +import { Format } from '@sinclair/typebox/format' // 0.25.0 + +import { FormatRegistry } from '@sinclair/typebox' // 0.26.0 +``` + + + +## Custom Renamed to TypeRegistry + +The `Format` module has been renamed to `FormatRegistry` and moved to the `typebox.ts` module. + +```typescript +import { Custom } from '@sinclair/typebox/format' // 0.25.0 + +import { TypeRegistry } from '@sinclair/typebox' // 0.26.0 +``` \ No newline at end of file diff --git a/changelog/0.26.2.md b/changelog/0.26.2.md new file mode 100644 index 000000000..052f4bd7d --- /dev/null +++ b/changelog/0.26.2.md @@ -0,0 +1,38 @@ +## [0.26.2](https://www.npmjs.com/package/@sinclair/typebox/v/0.26.2) + +Updates: + +- [331](https://github.com/sinclairzx81/typebox/pull/349) Revert 0.25.0 Intersect logic for Composite + +Notes: + +This PR reverts the logic on Type.Composite back to 0.25.0 Type.Intersect due to excessive type instantiation issues. On 0.26.0, Type.Composite attempted to take a union for overlapping properties, however due to the sophistication required to type map the unions for overlapping properties, this has resulted in type instantiation problems for some users upgrading to 0.26.0. + +As such, 0.26.2 reverts back to the 0.25.0 interpretation, but applies type mappings more inline with TS's interpretation of an overlapping varying property types. In the examples below, the type `C` is the evaluated type for Type.Composite. Note that TS will not union for overlapping properties and instead evaluate `never`. The 0.26.2 implementation falls inline with this evaluation. + +```typescript +{ // evaluation case 1: non-varying + type T = { a: number } & { a: number } + + type C = {[K in keyof T]: T[K] } // type C = { a: number } +} + +{ // evaluation case 2: varying + type T = { a: number } & { a: string } + + type C = {[K in keyof T]: T[K] } // type C = { a: never } +} + +{ // evaluation case 3: single optional + type T = { a?: number } & { a: number } + + type C = {[K in keyof T]: T[K] } // type C = { a: number } +} + +{ // evaluation case 4: all optional + type T = { a?: number } & { a?: number } + + type C = {[K in keyof T]: T[K] } // type C = { a?: number | undefined } +} +``` +Note: the Type.Composite is intended to be a temporary type which can be replaced with a more general `Type.Mapped` in future revisions of TypeBox. As the infrastructure to support mapped types does not exist, users can use Type.Composite to partially replicate mapped type evaluation for composited object types only. diff --git a/changelog/0.27.0.md b/changelog/0.27.0.md new file mode 100644 index 000000000..be87bbf32 --- /dev/null +++ b/changelog/0.27.0.md @@ -0,0 +1,152 @@ +## [0.27.0](https://www.npmjs.com/package/@sinclair/typebox/v/0.27.0) + +## Overview + +Revision 0.27.0 adds support for runtime [Template Literal Types](https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html). This revision does not include any functional breaking changes but does rename some public type aliases. As such this revision requires a minor semver increment. + +## Contents + +- Enhancements + - [Template Literal Types](#Template-Literal-Types) + - [TemplateLiteralParser](#TemplateLiteralParser) + - [WorkBench](#WorkBench) +- Breaking Changes + - [TSelf renamed to TThis](#TSelf-renamed-to-TThis) + + + + +## Template Literal Types + +Revision 0.27.0 adds support for Template Literal types. These types operate as a form of computed `TUnion>`. TypeBox encodes template literals using a subset of [ECMA 262](https://json-schema.org/understanding-json-schema/reference/regular_expressions.html) regular erxpressions which are applied to `pattern` properties of type `string`. This encoding enables JSON Schema validators to assert using existing regular expression checks. + +### TypeScript + +TypeScript defines Template Literals using back tick quoted strings which may include embedded union groups. + +```typescript +type T = `option${'A'|'B'}` // type T = 'optionA' | 'optionB' + +type R = Record // type R = { + // optionA: string + // optionB: string + // } +``` + +### TypeBox + +TypeBox defines Template Literals using the `TemplateLiteral` function. This function accepts a sequence of TLiteral, TString, TNumber, TInteger and TBigInt which describe a sequence concatenations. The embedded TUnion type defines an option group which can later be expanded into a set of `TLiteral`. This expansion enables Template Literal types to also be used as Record keys. + +```typescript +const T = Type.TemplateLiteral([ // const T = { + Type.Literal('option'), // pattern: '^option(A|B)$', + Type.Union([ // type: 'string' + Type.Literal('A'), // } + Type.Literal('B') + ]) +]) + +const R = Type.Record(T, Type.String()) // const R = { + // type: 'object', + // required: ['optionA', 'optionB'], + // properties: { + // optionA: { + // type: 'string' + // }, + // optionB: { + // type: 'string' + // } + // } + // } + +type T = Static // type T = 'optionA' | 'optionB' + +type R = Static // type R = { + // optionA: string + // optionB: string + // } +``` + +## TemplateLiteralParser + +Template Literal types are encoded as `string` patterns. Because these types also need to act as composable union types, Revision 0.27.0 includes an expression parser / generator system specifically for regular expressions. This system is used during composition to allow templates to compose with other types, but can also be used in isolation to generate string sequences for the supported expression grammar. This functionality may be provided as standard on the `Value.*` sub module in subsequent revisions. + +The following generates a 8-bit binary sequence for the given expression. + +```typescript + +import { TemplateLiteralParser, TemplateLiteralGenerator, TemplateLiteralFinite } from '@sinclair/typebox' + +const Bit = `(0|1)` // bit union +const Byte = `${Bit}${Bit}${Bit}${Bit}${Bit}${Bit}${Bit}${Bit}` // byte sequence + +const E = TemplateLiteralParser.Parse(Byte) // parsed expression tree +const F = TemplateLiteralFinite.Check(E) // is the expression finite? +const S = [...TemplateLiteralGenerator.Generate(E)] // generate sequence + +// const S = [ // computed sequence +// '00000000', '00000001', '00000010', '00000011', '00000100', +// '00000101', '00000110', '00000111', '00001000', '00001001', +// '00001010', '00001011', '00001100', '00001101', '00001110', +// '00001111', '00010000', '00010001', '00010010', '00010011', +// '00010100', '00010101', '00010110', '00010111', '00011000', +// '00011001', '00011010', '00011011', '00011100', '00011101', +// '00011110', '00011111', '00100000', '00100001', '00100010', +// '00100011', '00100100', '00100101', '00100110', '00100111', +// '00101000', '00101001', '00101010', '00101011', '00101100', +// '00101101', '00101110', '00101111', '00110000', '00110001', +// '00110010', '00110011', '00110100', '00110101', '00110110', +// '00110111', '00111000', '00111001', '00111010', '00111011', +// '00111100', '00111101', '00111110', '00111111', '01000000', +// '01000001', '01000010', '01000011', '01000100', '01000101', +// '01000110', '01000111', '01001000', '01001001', '01001010', +// '01001011', '01001100', '01001101', '01001110', '01001111', +// '01010000', '01010001', '01010010', '01010011', '01010100', +// '01010101', '01010110', '01010111', '01011000', '01011001', +// '01011010', '01011011', '01011100', '01011101', '01011110', +// '01011111', '01100000', '01100001', '01100010', '01100011', +// ... 156 more items +// ] +``` + + + +## Workbench + +To assist with TypeScript alignment and to prototype new features. A new web based compiler tool has been written that allows interactive cross compiling between TypeScript and TypeBox. This tool will be enhanced seperately from the TypeBox project, but can be used to quickly generate TypeBox type definitions from existing TypeScript types. + +[TypeBox Workbench Application](https://sinclairzx81.github.io/typebox-workbench) + +[TypeBox Workbench Project](https://github.com/sinclairzx81/typebox-workbench) + + + + +## Breaking Changes + +The following are breaking changes in Revision 0.27.0 + + + +## TSelf renamed to TThis + +This rename is to align with TypeScript interfaces. Unlike `type` aliases, TypeScript `interface` types include a implicit `this` type. This change relates specifically to TypeBox's current Recursive type which passes the `TThis` parameter via callback. The `TThis` parameter can be seen as analogous to the implicit TypeScript interface `this`. + +Consider the following. + +```typescript +// type T = { id: string, nodes: this[] } // error: no implicit this + +interface Node { // ok: this is implicit for interfaces + id: string, + nodes: this[] +} + +const T = Type.Recursive(This => // `This` === implicit 'this' for interface + Type.Object({ // + id: Type.String(), // Should `Recursive` be renamed to `Interface`? + nodes: Type.Array(This) + }) +) +``` +Future revisions may rename `Recurisve` to `Interface`, but for now, just the `TSelf` has been renamed. \ No newline at end of file diff --git a/changelog/0.27.1.md b/changelog/0.27.1.md new file mode 100644 index 000000000..eba9126bd --- /dev/null +++ b/changelog/0.27.1.md @@ -0,0 +1,5 @@ +## [0.27.1](https://www.npmjs.com/package/@sinclair/typebox/v/0.27.1) + +## Updates + +- Adds a `Value.Mutate(left, right)` function. This function performs a deep mutable assignment on a value by internally remapping the `right` values on the `left`. Values omitted on the right will also be deleted on the left. This function can be useful scenarios where mutation of data is required without replacing existing reference values. An example of which might be React which tracks reference values to indicate redraw. This function is implemented by way of `ValuePointer`. diff --git a/changelog/0.28.0.md b/changelog/0.28.0.md new file mode 100644 index 000000000..3ed1851fe --- /dev/null +++ b/changelog/0.28.0.md @@ -0,0 +1,199 @@ +## [0.28.0](https://www.npmjs.com/package/@sinclair/typebox/v/0.28.0) + +## Overview + +Revision 0.28.0 adds support for Indexed Access Types. This update also includes moderate breaking changes to Record and Composite types and does require a minor semver revision tick. + +## Contents + +- Enhancements + - [Indexed Access Types](#Indexed-Access-Types) + - [KeyOf Tuple and Array](#KeyOf-Tuple-and-Array) +- Breaking Changes + - [Record Types Allow Additional Properties By Default](#Record-Types-Allow-Additional-Properties-By-Default) + - [Composite Returns Intersect for Overlapping Properties](#Composite-Returns-Intersect-for-Overlapping-Properties) + + + +## Indexed Access Types + +Revision 0.28.0 adds [Indexed Access Type](https://www.typescriptlang.org/docs/handbook/2/indexed-access-types.html) support with a new `Type.Index()` mapping type. These types allow for deep property lookups without needing to prop dive through JSON Schema properties. This type is based on the TypeScript implementation of Indexed Access Types and allows for generalized selection of properties for complex types irrespective of if that type is a Object, Union, Intersection, Array or Tuple. + +```typescript +// ---------------------------------------------------------- +// The following types A and B are structurally equivalent, +// but have varying JSON Schema representations. +// ---------------------------------------------------------- +const A = Type.Object({ + x: Type.Number(), + y: Type.String(), + z: Type.Boolean(), +}) + +const B = Type.Intersect([ + Type.Object({ x: Type.Number() }), + Type.Object({ y: Type.String() }), + Type.Object({ z: Type.Boolean() }) +]) + +// ---------------------------------------------------------- +// TypeBox 0.27.0 - Non Uniform +// ---------------------------------------------------------- +const A_X = A.properties.x // TNumber +const A_Y = A.properties.y // TString +const A_Z = A.properties.z // TBoolean + +const B_X = B.allOf[0].properties.x // TNumber +const B_Y = B.allOf[1].properties.y // TString +const B_Z = B.allOf[2].properties.z // TBoolean + +// ---------------------------------------------------------- +// TypeBox 0.28.0 - Uniform via Type.Index +// ---------------------------------------------------------- +const A_X = Type.Index(A, ['x']) // TNumber +const A_Y = Type.Index(A, ['y']) // TString +const A_Z = Type.Index(A, ['z']) // TBoolean + +const B_X = Type.Index(B, ['x']) // TNumber +const B_Y = Type.Index(B, ['y']) // TString +const B_Z = Type.Index(B, ['z']) // TBoolean +``` +Indexed Access Types support has also been extended to Tuple and Array types. +```typescript +// ----------------------------------------------------------- +// Array +// ----------------------------------------------------------- +type T = string[] + +type I = T[number] // type T = string + +const T = Type.Array(Type.String()) + +const I = Type.Index(T, Type.Number()) // const I = TString + +// ----------------------------------------------------------- +// Tuple +// ----------------------------------------------------------- +type T = ['A', 'B', 'C'] + +type I = T[0 | 1] // type I = 'A' | 'B' + +const T = Type.Array(Type.String()) + +const I = Type.Index(T, Type.Union([ // const I = TUnion<[ + Type.Literal(0), // TLiteral<'A'>, + Type.Literal(1), // TLiteral<'B'> +])) // ]> +``` + + + +## KeyOf Tuple and Array + +Revision 0.28.0 includes additional `Type.KeyOf` support for Array and Tuple types. Keys of Array will always return `TNumber`, whereas keys of Tuple will return a LiteralUnion for each index of that tuple. + +```typescript +// ----------------------------------------------------------- +// KeyOf: Tuple +// ----------------------------------------------------------- +const T = Type.Tuple([Type.Number(), Type.Number(), Type.Number()]) + +const K = Type.KeyOf(T) // const K = TUnion<[ + // TLiteral<'0'>, + // TLiteral<'1'>, + // TLiteral<'2'>, + // ]> + +// ----------------------------------------------------------- +// KeyOf: Array +// ----------------------------------------------------------- +const T = Type.Array(Type.String()) + +const K = Type.KeyOf(T) // const K = TNumber +``` +It is possible to combine KeyOf with Index types to extract properties from array and object constructs. +```typescript +// ----------------------------------------------------------- +// KeyOf + Index: Object +// ----------------------------------------------------------- +const T = Type.Object({ x: Type.Number(), y: Type.String(), z: Type.Boolean() }) + +const K = Type.Index(T, Type.KeyOf(T)) // const K = TUnion<[ + // TNumber, + // TString, + // TBoolean, + // ]> + +// ----------------------------------------------------------- +// KeyOf + Index: Tuple +// ----------------------------------------------------------- +const T = Type.Tuple([Type.Number(), Type.String(), Type.Boolean()]) + +const K = Type.Index(T, Type.KeyOf(T)) // const K = TUnion<[ + // TNumber, + // TString, + // TBoolean, + // ]> +``` +## Breaking Changes + +The following are breaking changes in Revision 0.28.0 + + + +## Record Types Allow Additional Properties By Default + +Revision 0.28.0 no longer applies an automatic `additionalProperties: false` constraint to types of `TRecord`. Previously this constraint was set to prevent records with numeric keys from allowing unevaluated additional properties with non-numeric keys. This constraint worked in revisions up to 0.26.0, but since the move to use `allOf` intersect schema representations, this meant that types of Record could no longer be composed with intersections. This is due to the JSON Schema rules around extending closed schemas. Information on these rules can be found at the link below. + +https://json-schema.org/understanding-json-schema/reference/object.html#extending-closed-schemas + +For the most part, the omission of this constraint shouldn't impact existing record types with string keys, however numeric keys may cause problems. Consider the following where the validation unexpectedly succeeds for the following numeric keyed record. + +```typescript +const T = Type.Record(Type.Number(), Type.String()) + +const R = Value.Check(T, { a: null }) // true - Because `a` is non-numeric and thus is treated as an + // additional unevaluated property. +``` +Moving forward, Records with numeric keys "should" be constrained explicitly with `additionalProperties: false` via options if that record does not require composition through intersection. This is largely inline with the existing constraints one might apply to types of Object. + +```typescript +const T = Type.Record(Type.Number(), Type.String(), { + additionalProperties: false +}) + +const R = Value.Check(T, { a: null }) // false - Because `a` is non-numeric additional property +``` + + + +## Composite Returns Intersect for Overlapping Properties + +This is a minor breaking change with respect to the schema returned for Composite objects with overlapping varying property types. Previously TypeBox would evaluate `TNever` by performing an internal `extends` check against each overlapping property type. However problems emerged using this implementation for users who needed to use Composite with types of `TUnsafe`. This is due to unsafe types being incompatible with TypeBox's internal extends logic. + +The solution implemented in 0.28.0 is to return the full intersection of all overlapping properties. The reasoning here is that if the overlapping properties of varying types result in an illogical intersection, this is semantically the same as resolving `never` for that property. This approach avoids the need to internally check if all overlapping properties extend or narrow one another. + +```typescript +const T = Type.Composite([ + Type.Object({ x: Type.Number() }), // overlapping property 'x' of varying type + Type.Object({ x: Type.String() }) +]) + +// ----------------------------------------------------------- +// Revision 0.27.0 +// ----------------------------------------------------------- +const R = Type.Object({ + x: Type.Never() // Never evaluated through extends checks. +}) + +// ----------------------------------------------------------- +// Revision 0.28.0 +// ----------------------------------------------------------- +const R = Type.Object({ + x: Type.Intersect([ // Illogical intersections are semantically the same as never + Type.Number(), + Type.String() + ]) +}) +``` +This implementation should make it more clear what the internal mechanics are for object compositing. Future revisions of TypeBox may however provide a utility function to test illogical intersections for Never for known types. \ No newline at end of file diff --git a/changelog/0.28.3.md b/changelog/0.28.3.md new file mode 100644 index 000000000..876b7e3fb --- /dev/null +++ b/changelog/0.28.3.md @@ -0,0 +1,57 @@ +## [0.28.3](https://www.npmjs.com/package/@sinclair/typebox/v/0.28.3) + +## Overview + +Revision 0.28.3 adds a new Rest type to support variadic type composition. + +## Contents + +- Enhancements + - [Variadic Types](#Variadic-Types) + + + +## Variadic Types + +Revision 0.28.3 adds a new type named `Type.Rest`. This type is used to extract a tuple array of type of `[...TSchema]`. The return value of this type is not strictly JSON Schema, however the tuple array can be used as a parameter to other types that accept tuples as their arguments. + +### Tuple Concatenation + +```typescript +// TypeScript + +type A = [1, 2] + +type B = [3, 4] + +type C = [...A, ...B] + +// TypeBox + +const A = Type.Tuple([Type.Literal(1), Type.Literal(2)]) + +const B = Type.Tuple([Type.Literal(3), Type.Literal(4)]) + +const C = Type.Tuple([...Type.Rest(A), ...Type.Rest(B)]) +``` + +### Tuple To Parameter + +```typescript +// TypeScript + +type P = [number, number] + +type F1 = (param: [...P]) => void + +type F2 = (param: [...P, number]) => void + +// TypeBox + +const P = Type.Tuple([Type.Number(), Type.Number()]) + +const F1 = Type.Function(Type.Rest(P), Type.Void()) + +const F2 = Type.Function([...Type.Rest(P), Type.Number()], Type.Void()) +``` + diff --git a/changelog/0.29.0.md b/changelog/0.29.0.md new file mode 100644 index 000000000..1291219d7 --- /dev/null +++ b/changelog/0.29.0.md @@ -0,0 +1,144 @@ +## [0.29.0](https://www.npmjs.com/package/@sinclair/typebox/v/0.29.0) + +## Overview + +Revision 0.29.0 makes a minor interface and schema representation change to the `Type.Not` type. This revision also includes a fix for indexed access types on TypeScript 5.1.6. + +As this revision constitutes a breaking representation change for `Type.Not`, a minor semver revision is required. + +## Contents + +- Enhancements + - [Type.Not Representation Change](#Representation-Change) + - [Not Inversion](#Not-Inversion) + - [Inference Limitations](#Inference-Limitations) + + + +## Type.Not Representation Change + +The `Type.Not` was first introduced in Revision 0.26.0. This type accepted two arguments, the first is the `not` type, the second is the `allowed` type. In 0.26.0, TypeBox would treat the `allowed` type as the inferred type with the schema represented in the following form. + +### 0.26.0 + +```typescript +// allow all numbers except the number 42 +// +const T = Type.Not(Type.Literal(42), Type.Number()) +// ^ ^ +// not type allowed type + +// represented as +// +const T = { + allOf: [ + { not: { const: 42 } }, + { type: 'number' } + ] +} + +// inferred as +// +type T = Static // type T = number +``` +In 0.26.0. the rationale for the second `allowed` argument was provide a correct static type to infer, where one could describe what the type wasn't on the first and what it was on the second (with inference of operating on the second argument). This approach was to echo possible suggestions for negated type syntax in TypeScript. + +```typescript +type T = number & not 42 // not actual typescript syntax! +``` + +### 0.29.0 + +Revision 0.29.0 changes the `Type.Not` type to take a single `not` argument only. This type statically infers as `unknown` + +```typescript +// allow all types except the literal number 42 +// +const T = Type.Not(Type.Literal(42)) +// ^ +// not type + +// represented as +// +const T = { not: { const: 42 } } + +// inferred as +// +type T = Static // type T = unknown + +``` +### Upgrading to 0.29.0 + +In revision 0.29.0, you can express the 0.26.0 Not type via `Type.Intersect` which explicitly creates the `allOf` representation. The type inference works in this case as intersected `number & unknown` yields the most narrowed type (which is `number`) + +```typescript +// allow all numbers except the number 42 +// +const T = Type.Intersect([ Type.Not(Type.Literal(42)), Type.Number() ]) +// ^ ^ +// not type allowed type + +// represented as +// +const T = { + allOf: [ + { not: { const: 42 } }, + { type: 'number' } + ] +} +// inferred as +// +type T = Static // type T = number +``` +The 0.29.0 `Not` type properly represents the JSON Schema `not` keyword in its simplest form, as well as making better use of intersection type narrowing capabilities of TypeScript. + + + +## Not Inversion + +The not type can be inverted through nesting. + +```typescript +// not not string +// +const T = Type.Not(Type.Not(Type.String())) + +// represented as +// +const T = { + not: { + not: { + type: "string" + } + } +} + +// inferred as +// +type T = Static // type T = string +``` + + + +## Inference Limitations + +Not types are synonymous with the concept of [negated types](https://github.com/microsoft/TypeScript/issues/4196) which are not supported in the TypeScript language. Because of this, it is not currently possible to infer negated types in a way one would naturally expect for some cases. Consider the following. + +```typescript +const T = Type.Intersect([Type.String(), Type.Not(Type.String())]) + +type T = Static // type T = string & not string + // actual: string + // expect: never +``` +As such, the use of Not types should be used with some consideration to current limitations, and reserved primarily for narrowing cases such as the following. + +```typescript +const T = Type.Intersect([Type.String(), Type.Not(Type.Literal('disallowed string'))]) + +type T = Static // type T = string & not 'disallowed string' + // actual: string + // expect: string +``` + + diff --git a/changelog/0.29.2.md b/changelog/0.29.2.md new file mode 100644 index 000000000..f87cfc217 --- /dev/null +++ b/changelog/0.29.2.md @@ -0,0 +1,141 @@ +## [0.29.2](https://www.npmjs.com/package/@sinclair/typebox/v/0.29.2) + +## Overview + +Revision 0.29.2 includes enhancements to `Type.Index` + +This revision contains no breaking changes + +## Contents + +- Enhancements + - [Modifier Index Resolver](#Modifier-Index-Resolver) + - [Indexed Intersect](#Indexed-Intersect) + - [Indexed Union](#Indexed-Union) + - [Composite](#Composite) + + + +## Modifier Index Resolver + +Revision 0.29.2 re-introduces optional property narrowing for Indexed Access Types. This functionality is specific when indexing overlapping properties with one or more optional modifiers. Revision 0.28.0 attempted to implement this narrowing, however was pulled due to instantiation issues raised on issue [419](https://github.com/sinclairzx81/typebox/issues/419) (specific to composite narrowing) + +Revision 0.29.2 attempts to re-introduce this functionality using a different resolution strategy. It uses Indexed Access Types as the construct in which to apply such narrowing and expands upon Union and Intersection type normalization for optional modifier unwrap and remapping. This approach unifies Composite inference with Index Access Types and makes provisions for a possible `Type.Mapped` feature in later releases. + + + +## Indexed Intersect + +The following are the index resolver cases for intersect types. + +### Case 1 +```typescript +const T = Type.Intersect([ + Type.Object({ x: Type.Optional(Type.Number()) }), + Type.Object({ x: Type.Optional(Type.Number()) }) +]) + +const I = Type.Index(T, ['x']) + +// type I = TOptional> +``` +### Case 2 +```typescript +const T = Type.Intersect([ + Type.Object({ x: Type.Optional(Type.Number()) }), + Type.Object({ x: Type.Number() }) +]) + +const I = Type.Index(T, ['x']) + +// type I = TUnion<[TNumber, TNumber]> +``` +### Case 3 +```typescript +const T = Type.Intersect([ + Type.Object({ x: Type.Number() }), + Type.Object({ x: Type.Number() }) +]) + +const I = Type.Index(T, ['x']) + +// type I = TUnion<[TNumber, TNumber]> +``` + + +## Indexed Union + +The following are the index resolver cases for union types. + +### Case 1 +```typescript +const T = Type.Union([ + Type.Object({ x: Type.Optional(Type.Number()) }), + Type.Object({ x: Type.Optional(Type.Number()) }) +]) + +const I = Type.Index(T, ['x']) + +// type I = TOptional> +``` +### Case 2 +```typescript +const T = Type.Union([ + Type.Object({ x: Type.Optional(Type.Number()) }), + Type.Object({ x: Type.Number() }) +]) + +const I = Type.Index(T, ['x']) + +// type I = TOptional> +``` +### Case 3 +```typescript +const T = Type.Union([ + Type.Object({ x: Type.Number() }), + Type.Object({ x: Type.Number() }) +]) + +const I = Type.Index(T, ['x']) + +// type I = TUnion<[TNumber, TNumber]> +``` + + +## Composite + +The following are the resolver cases for indexed types when applied to composite intersection. + +### Case 1 +```typescript +const T = Type.Composite([ + Type.Object({ x: Type.Optional(Type.Number()) }), + Type.Object({ x: Type.Optional(Type.Number()) }) +]) + +// type T = TObject<{ +// x: TOptional> +// }> +``` +### Case 2 +```typescript +const T = Type.Composite([ + Type.Object({ x: Type.Optional(Type.Number()) }), + Type.Object({ x: Type.Number() }) +]) + +// type T = TObject<{ +// x: TUnion<[TNumber, TNumber]> +// }> +``` +### Case 3 +```typescript +const T = Type.Composite([ + Type.Object({ x: Type.Number() }), + Type.Object({ x: Type.Number() }) +]) + +// type T = TObject<{ +// x: TUnion<[TNumber, TNumber]> +// }> +``` \ No newline at end of file diff --git a/changelog/0.30.0.md b/changelog/0.30.0.md new file mode 100644 index 000000000..31899d3fe --- /dev/null +++ b/changelog/0.30.0.md @@ -0,0 +1,419 @@ +## [0.30.0](https://www.npmjs.com/package/@sinclair/typebox/v/0.30.0) + +## Overview + +Revision 0.30.0 is a milestone revision for the TypeBox project. It is primarily focused on internal optimizations, refactoring work to reduce package and bundle sizes, enable increased modularity of internal sub modules (with some considerations given to future ESM publishing), renaming internal functions to address react native bundling issues and consolidating shared internal modules to reduce overall code overhead. + +This revision also implements several new features, including new validation constraints for Array, new types for iterators, new utility types, a TypeScript code generation option for the compiler, enhancements made to modifiers and better options for TypeScript to TypeBox code translation. This revision also includes new examples including a transform type for handling IO encode an decode as well as a reference implementation for JSON Type Definition specification. + +This revision includes breaking changes and some deprecations. It requires a minor semver revision. + +## Contents + +- Enhancements + - [TypeScript Code Generation](#TypeScript-Code-Generation) + - [Optional and Readonly](#Optional-and-Readonly) + - [Iterator and AsyncIterator](#Iterator-and-AsyncIterator) + - [Order Independent References](#Order-Independent-References) + - [Value Submodules](#Value-Submodules) + - [Array Contains Constraint](#Array-Contains-Constraint) + - [Additional Utility Types](#Additional-Utility-Types) + - [Reduced Package Size](#Reduced-Package-Size) + - [TypeBox Codegen](#TypeBox-Codegen) +- Examples + - [JSON Type Definition](#JSON-Type-Definition) + - [Prototype Types](#Prototype-Types) + - [Transform Types](#Transform-Types) +- Breaking + - [Extended Type Representation-Change](#Extended-Type-Representation-Change) + - [Modifier Symbol Deprecated](#Modifier-Deprecated) + - [RegEx Renamed To RegExp](#RegEx-Renamed-To-RegExp) + - [ValueErrorType Custom Renamed To Kind](#ValueErrorType-Custom-Renamed-To-Kind) + + + +## TypeScript Code Generation + +Revision 0.30.0 adds TypeScript code generation support to the TypeCompiler. By specifying the language option on the `.Code()` function, TypeBox will add type annotations to the compiled output. This functionality can be used to produce typed TS functions for projects that preference AOT compilation. + +```typescript +const Code = TypeCompiler.Code(Type.String(), { // return function check(value: any): boolean { + language: 'typescript' // return ( +}) // (typeof value === 'string') + // ) + // } +``` + + + +## Optional and Readonly + +Revision 0.30.0 deprecates the `[Modifier]` symbol and introduces two new symbols, `[Readonly]` and `[Optional]`. This change is carried out to simplify type inference as well as to simplify runtime mapping logic. This change should not implicate users leveraging the `Type.*` purely for type composition, however implementors using TypeBox for reflection and code generation should update to the new symbols. + +```typescript +// Revision 0.29.0 +// +const A = Type.ReadonlyOptional(Type.Number()) // const A: TReadonlyOptional = { + // type: 'number', + // [TypeBox.Modifier]: 'ReadonlyOptional' + // } + +const B = Type.Readonly(Type.Number()) // const B: TReadonly = { + // type: 'number', + // [TypeBox.Modifier]: 'Readonly' + // } + +const C = Type.Optional(Type.Number()) // const C: TOptional = { + // type: 'number', + // [TypeBox.Modifier]: 'Optional' + // } + +// Revision 0.30.0 +// +const A = Type.ReadonlyOptional(Type.Number()) // const A: TReadonly> = { + // type: 'number', + // [TypeBox.Readonly]: 'Readonly', + // [TypeBox.Optional]: 'Optional' + // } + +const B = Type.Readonly(Type.Number()) // const B: TReadonly = { + // type: 'number', + // [TypeBox.Readonly]: 'Readonly' + // } + +const C = Type.Optional(Type.Number()) // const C: TOptional = { + // type: 'number', + // [TypeBox.Optional]: 'Optional' + // } +``` + + + +## Iterator and AsyncIterator + +Revision 0.30.0 adds the types `Iterator` and `AsyncIterator`. These types add to the existing non-validatable extended type set and can be used build callable generator functions. These types are written primarily to describe RPC network interfaces that return multiple values. Examples of which may include web socket streams or reading database result cursors over a network. + +```typescript +// Revision 0.30.0 +// +const Enumerable = (T: T) => Type.Function([ + Type.Number({ description: 'Start index' }), + Type.Number({ description: 'End index' }) +], Type.Iterator(T)) + +const EnumerableNumber = Enumerable(Type.Number()) + +const Range: Static = function * (start: number, end: number) { + for(let i = start; i < end; i++) yield i +} + +const R = [...Range(10, 20)] // const R = [10, 11, 12, ..., 19] +``` + + + +## Order Independent References + +Revision 0.30.0 adds an overload for `Ref` to enable non order dependent type referencing. Prior to this revision, reference targets needed to be defined first before being referenced. Revision 0.30.0 lifts this restriction and allows referencing of "yet to be defined" targets through the use of `typeof` operator. This overload borrows on TypeScript's ability to derive type information irrespective of topological ordering. + +This overload is implemented for "TypeScript to TypeBox" code generation utilities where TypeScript types are not guaranteed ordered in a runtime sorted fashion. + +```typescript +// Revision 0.29.0 +// +const R = Type.Ref(T) // Error: T isn't defined yet + +const T = Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number() +}, { $id: 'T' }) + +// Revision 0.30.0 +// +const R = Type.Ref('T') // Ok: infer from typeof T + +const T = Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number() +}, { $id: 'T' }) +``` + + + +## Value Submodules + +Revision 0.30.0 carries out a number of refactorings for the `Value.*` modules to enable each submodule to be imported individually. These refactorings are support better "pay to play" library characteristics, allowing users to import only the submodules they need. This update also makes provisions for ESM publishing by removing internal namespaces. + +The top level `Value.*` namespace will remain on all subsequent versions of TypeBox. + +```typescript +// Revision 0.29.0 +// +import { Value } from '@sinclair/typebox/value' // Value.* namespace + +const A = Value.Create(Type.String()) + +// Revision 0.30.0 +// +import { Create } from '@sinclair/typebox/value/create' // Only Create() + +const A = Create(Type.String()) +``` + + + +## Array Contains Constraint + +Revision 0.30.0 implements validation support for the `contains` keyword as well as the draft 2019-09 `minContains` and `maxContains` constraints on `Array`. Documentation on these constraints can be found https://json-schema.org/understanding-json-schema/reference/array.html#contains + +```typescript +// Revision 0.30.0 +// +const T = Type.Array(Type.Number(), { + contains: Type.Literal(1), + minContains: 3, + maxContains: 5 +}) + +Value.Check(T, [1, 1, 1]) // true - between 3 and 5 instances of 1 +Value.Check(T, [1, 1, 1, 1, 1]) // true - between 3 and 5 instances of 1 +Value.Check(T, [0, 1, 1, 1, 1, 1]) // true - between 3 and 5 instances of 1 +Value.Check(T, [1, 1]) // false - less than 3 instances of 1 +Value.Check(T, [1, 1, 1, 1, 1, 1]) // false - more than 5 instances of 1 +Value.Check(T, [0]) // false - no instances of 1 +``` + + + +## Additional Utility Types + +Revision 0.30.0 adds the utility types `Awaited`, `Uppercase`, `Lowercase`, `Capitalize`, and `Uncapitalize` to the supported type set. + +```typescript +// Revision 0.30.0 +const T1 = Type.Awaited(Type.Promise(Type.String())) // const T1: TString + +const T2 = Type.Uppercase(Type.Literal('hello')) // const T2: TLiteral<'HELLO'> + +const T3 = Type.Lowercase(Type.Literal('HELLO')) // const T3: TLiteral<'hello'> + +const T4 = Type.Capitalize(Type.Literal('hello')) // const T4: TLiteral<'Hello'> + +const T5 = Type.Uncapitalize(Type.Literal('HELLO')) // const T5: TLiteral<'hELLO'> +``` +A full list of TypeScript utility types can be found at this [link](https://www.typescriptlang.org/docs/handbook/utility-types.html). + + + +## Reduced Package Size + +Revision 0.30.0 carries out several internal refactorings to reduce package and bundle sizes. This work is largely an ongoing process with provisional work carried out across `type`, `value` and `compiler` modules. Revision 0.30.0 manages to weigh in slightly less than Revision 0.29.0 with the additional functionality provided on the revision. + +```typescript +// Revision 0.29.0 +// +┌──────────────────────┬────────────┬────────────┬─────────────┐ +│ (index) │ Compiled │ Minified │ Compression │ +├──────────────────────┼────────────┼────────────┼─────────────┤ +│ typebox/compiler │ '130.3 kb' │ ' 58.2 kb' │ '2.24 x' │ +│ typebox/errors │ '113.3 kb' │ ' 49.8 kb' │ '2.27 x' │ +│ typebox/system │ ' 78.8 kb' │ ' 32.2 kb' │ '2.45 x' │ +│ typebox/value │ '180.0 kb' │ ' 77.7 kb' │ '2.32 x' │ +│ typebox │ ' 77.7 kb' │ ' 31.7 kb' │ '2.45 x' │ +└──────────────────────┴────────────┴────────────┴─────────────┘ + +// Revision 0.30.0 +// +┌──────────────────────┬────────────┬────────────┬─────────────┐ +│ (index) │ Compiled │ Minified │ Compression │ +├──────────────────────┼────────────┼────────────┼─────────────┤ +│ typebox/compiler │ '129.4 kb' │ ' 58.6 kb' │ '2.21 x' │ +│ typebox/errors │ '111.6 kb' │ ' 50.1 kb' │ '2.23 x' │ +│ typebox/system │ ' 76.5 kb' │ ' 31.7 kb' │ '2.41 x' │ +│ typebox/value │ '180.7 kb' │ ' 79.3 kb' │ '2.28 x' │ +│ typebox │ ' 75.4 kb' │ ' 31.3 kb' │ '2.41 x' │ +└──────────────────────┴────────────┴────────────┴─────────────┘ +``` + + + +## TypeBox Codegen + +Revision 0.30.0 offers an external code generation API tool which can be used to programmatically convert TypeScript types into TypeBox types. + +[TypeBox-Code Project](https://github.com/sinclairzx81/typebox-codegen) + +```typescript +import * as Codegen from '@sinclair/typebox-codegen' + +const Code = Codegen.TypeScriptToTypeBox.Generate(` + type T = { x: number, y: number, z: number } +`) + +console.log(Code) + +// Output: +// +// import { Type, Static } from '@sinclair/typebox' +// +// type T = Static +// const T = Type.Object({ +// x: Type.Number(), +// y: Type.Number(), +// z: Type.Number() +// }) +``` + + + +## JSON Type Definition + +Revision 0.30.0 includes a reference implementation for JSON Type Definition (RFC 8927). This specification is currently under consideration for inclusion in the TypeBox library as an alternative schema representation for nominal type systems. The implementation currently contains all types expressed in the JSON Type Definition spec, but omits constraints such and `minimum` and `maximum` values (which are not formally represented in the specification). + +The implementation is offered as a single file which can be copied in to projects with TypeBox installed. This implementation may be enhanced over the next few revisions (with some potential to implement mapping types such as partial, required, omit, pick, keyof). This specification will be considered for inclusion under `@sinclair/typebox/typedef` if there is enough interest. + +```typescript +import { Type } from './typedef' // from: examples/typedef/typedef.ts + +const T3 = Type.Struct({ // const T3 = { + x: Type.Float32(), // properties: { + y: Type.Float32(), // x: { type: 'float32' }, + z: Type.Float32() // y: { type: 'float32' }, +}) // z: { type: 'float32' } + // } + // } + +const T2 = Type.Struct({ // const T3 = { + x: Type.Float32(), // properties: { + y: Type.Float32() // x: { type: 'float32' }, +}) // y: { type: 'float32' } + // } + // } + +const U = Type.Union([ // const U = { + T3, // discriminator: 'type', + T2 // mapping: { +]) // 0: { + // properties: { + // x: { type: 'float32' }, + // y: { type: 'float32' }, + // z: { type: 'float32' } + // } + // }, + // 1: { + // properties: { + // x: { type: 'float32' }, + // y: { type: 'float32' } + // } + // } + // } + // } +``` + + + +## Prototype Types + +Revision 0.30.0 renames `Experimental` types to `Prototype` types within the examples directory. Updates here include additional documentation and rationales for the existing types `UnionOneOf`, `UnionEnum`, `Const`, and includes two new types `Evaluate` and `PartialDeep`. These types are written as standalone modules and can be copied into a project for direct use. The TypeBox project is open to community discussions around the inclusion of these types in future revisions. + + + +## Transform Types + +Revision 0.30.0 provides a reference implementation for Transform types. There has been some interest from users to offer combinators similar to Zod's `.transform()` function that permits remapping values during `.parse()` like operations. As TypeBox types do not have fluent combinators or a parse function (and are just JSON Schema objects), introducing similar functionality without augmenting types or implementing a `.parse()` on all types has proven to be particularily challenging. + +The reference Transform implementation implements a workable design by augmenting TypeBox types with codec functions outside the type system. These functions allow values to be structurally encoded and decoded through the `.parseLike()` functions `Encode()` and `Decode()`. TypeBox adopts the `io-ts` perspective for value transformation, viewing the act of transforming values primarily the role of dedicated codec system. As much of this functionality is considered high level and above and beyond the type system, Transform types will not likely be added to TypeBox type system; but rather added as an optional import in later revisions. + +```typescript +import { Transform, Encode, Decode } from './transform' + +const Timestamp = Transform(Type.Number(), { // The Transform function wraps a TypeBox type with two codec + decode: (value) => new Date(value), // functions which implement logic to decode a received value + encode: (value) => value.getTime(), // (i.e. number) into a application type (Date). The encode +}) // function handles the reverse mapping. + +type N = Static // type N = { timestamp: number } + // +const N = Type.Object({ // Transform types are to be used like any other type and will + timestamp: Timestamp // infer as the original TypeBox type. For example, the type `N` +}) // above will infer as { timestamp: number } (as derived from + // the TB type) + + + +const D = Decode(N, { timestamp: 123 }) // const D = { timestamp: Date(123) } + // + // The Decode function accepts any type plus a value. The Decode + // function return type will be that of the transforms decode() + // return type (which is Date), with the second argument statically + // typed as N. This function acts as a kind of parse() that returns + // the decoded type or throws on validation error. + + +const E = Encode(N, { timestamp: new Date(123) }) // const E = { timestamp: 123 } + // + // The encode function performs the inverse, accepting the + // decoded type { timestamp: Date } and re-encoding to the + // target type { timestamp: number }. This function will + // also throw on validation error. +``` + + + +## Extended Type Representation Change + +Revision 0.30.0 updates representations for all extended types. This change is made due to TypeBox's observed role as a general purpose JavaScript validation library as well as to deprecate support for extended type validation in Ajv which was only ever partially functional at best. + +Attempts were made on Revision 0.25.0 to restructure extended types to provide Ajv hooks for custom type configuration. These hooks used the `type` property where `{ type: 'object', instanceOf: 'Type' }` was used to configure schematics for JavaScript objects, and `{ type: 'null', typeOf: 'Type' }` was used for JavaScript primitives. Despite these hooks, Ajv would still struggle with validation of primitive types (such as `undefined`), and for the types `Function`, `Constructor` and `Promise`; these were meaningless to Ajv and it did not make sense to try provide hooks for a validator that could not make use of them. + +This change represents a move towards a formal specification to express pure JavaScript constructs which is partially under discussion within the runtime type community. This change will implicate the use of `Uint8Array` and `Date` objects when configuring for Ajv. A supplimentary fallback will be provided in the `/examples` directory using `Type.Unsafe` + +```typescript +// Revision 0.29.0 +// +const T = Type.Date() // const T: TDate = { type: 'object', instanceOf: 'Date' } + +const U = Type.Undefined() // const U: TUndefined = { type: 'null', typeOf: 'Undefined' } + +// Revision 0.30.0 +// +const T = Type.Date() // const T: TDate = { type: 'Date' } + +const U = Type.Undefined() // const U: TUndefined = { type: 'undefined' } +``` + + + +## RegEx Renamed To RegExp + +Revision 0.30.0 marks `Type.RegEx` as deprecated but provides `Type.RegExp` as an alternative (matching the JavaScript `RegExp` type name). Additionally this type has also been moved from the `Standard` to `Extended` type set. The `RegExp` type will no longer considered part of the Standard type set due to JavaScript Regular Expressions supporting a wider range of symbols and control characeters than is supported by the ECMA262 subset used by the JSON Schema specification. Information on the ECMA262 subset supported by JSON Schema can be found at the following Url https://json-schema.org/understanding-json-schema/reference/regular_expressions.html + +As `Type.RegEx()` is widely used, this function will be retained under the `@deprecated` annotation for the 0.30.0 revision. + +```typescript +// Revision 0.29.0 + +const T = Type.RegEx(/abc/) + +// Revision 0.30.0 + +const A = Type.RegEx(/abc/) // deprecation warning! + +const B = Type.RegExp(/abc/) // Extended Type + +const T = Type.String({ pattern: /abc/.source }) // Standard Type +``` +For Unicode (UTF-16) support on 0.30.0, the recommendation is to continue using user defined formats. + +```typescript +import { Type, FormatRegistry } from '@sinclair/typebox' + +FormatRegistry.Set('emoji', value => /|\p{Extended_Pictographic}/gu.test(value)) + +const T = Type.String({ format: 'emoji' }) + +Value.Check(T, '♥️♦️♠️♣️') // Ok +``` +For information on configuring custom formats on Ajv, refer to https://ajv.js.org/guide/formats.html#user-defined-formats diff --git a/changelog/0.31.0.md b/changelog/0.31.0.md new file mode 100644 index 000000000..4dc83aa48 --- /dev/null +++ b/changelog/0.31.0.md @@ -0,0 +1,331 @@ +## [0.31.0](https://www.npmjs.com/package/@sinclair/typebox/v/0.31.0) + +## Overview + +Revision 0.31.0 is a subsequent milestone revision for the TypeBox library and a direct continuation of the work carried out in 0.30.0 to optimize and prepare TypeBox for a 1.0 release candidate. This revision implements a new codec system with Transform types, provides configurable error message generation for i18n support, adds a library wide exception type named TypeBoxError and generalizes the Rest type to enable richer composition. This revision also finalizes optimization work to reduce the TypeBox package size. + +This revision contains relatively minor breaking changes due to internal type renaming. A minor semver revision is required. + +## Contents + +- Enhancements + - [Transform Types](#Transform-Types) + - [Encode and Decode](#Encode-and-Decode) + - [StaticEncode and StaticDecode](#StaticEncode-and-StaticDecode) + - [Rest Types](#Rest-Types) + - [Record Key](#Record-Key) + - [TypeBoxError](#TypeBoxError) + - [TypeSystemErrorFunction](#TypeSystemErrorFunction) + - [Reduce Package Size](#Reduce-Package-Size) +- Breaking + - [JsonTypeBuilder and JavaScriptTypeBuilder](#JsonTypeBuilder-and-JavaScriptTypeBuilder) + - [TypeSystemPolicy](#TypeSystemPolicy) + + + +## Transform Types + +Revision 0.31.0 includes a new codec system referred to as Transform types. A Transform type is used to augment a regular TypeBox type with Encode and Decode functions. These functions are invoked via the new Encode and Decode functions available on both Value and TypeCompiler modules. + +The following shows a Transform type which increments and decrements a number. + +```typescript +import { Value } from '@sinclair/typebox/value' + +const T = Type.Transform(Type.Number()) // const T = { + .Decode(value => value + 1) // type: 'number', + .Encode(value => value - 1) // [Symbol(TypeBox.Kind)]: 'Number', + // [Symbol(TypeBox.Transform)]: { + // Decode: [Function: Decode], + // Encode: [Function: Encode] + // } + // } + +const A = Value.Decode(T, 0) // const A: number = 1 + +const B = Value.Encode(T, 1) // const B: number = 0 +``` + + + +## Encode and Decode + +Revision 0.31.0 includes new functions to Decode and Encode values. These functions are written in service to Transform types, but can be used equally well without them. These functions return a typed value that matches the type being transformed. TypeBox will infer decode and encode differently, yielding the correct type as derived from the codec implementation. + +The following shows decoding and encoding between number to Date. Note these functions will throw if the value is invalid. + +```typescript +const T = Type.Transform(Type.Number()) + .Decode(value => new Date(value)) // number to Date + .Encode(value => value.getTime()) // Date to number + +// Ok +// +const A = Value.Decode(T, 42) // const A = new Date(42) + +const B = Value.Encode(T, new Date(42)) // const B = 42 + +// Error +// +const C = Value.Decode(T, true) // Error: Expected number + +const D = Value.Encode(T, 'not a date') // Error: getTime is not a function +``` +The Decode function is extremely fast when decoding regular TypeBox types; and TypeBox will by pass codec execution if the type being decoded contains no interior Transforms (and will only use Check). When using Transforms however, these functions may incur a performance penelty due to codecs operating structurally on values using dynamic techniques (as would be the case for applications manually decoding values). As such the Decode design is built to be general and opt in, but not necessarily high performance. + + + +## StaticEncode and StaticDecode + +Revision 0.31.0 includes new inference types `StaticEncode` and `StaticDecode`. These types can be used to infer the encoded and decoded states of a Transform as well as regular TypeBox types. These types can be used to replace `Static` for `Request` and `Response` inference pipelines. + +The following shows an example `Route` function that uses Transform inference via `StaticDecode`. + +```typescript +// Route +// +export type RouteCallback = + (request: StaticDecode) => StaticDecode // replace Static with StaticDecode + +export function Route( + path: TPath, + requestType: TRequest, + responseType: TResponse, + callback: RouteCallback +) { + // route handling here ... + + const input = null // receive input + const request = Value.Decode(requestType, input) + const response = callback(request) + const output = Value.Encode(responseType, response) + // send output +} + +// Route: Without Transform +// +const Timestamp = Type.Number() + +Route('/exampleA', Timestamp, Timestamp, (value) => { + return value // value observed as number +}) + +// Route: With Transform +// +const Timestamp = Type.Transform(Type.Number()) + .Decode(value => new Date(value)) + .Encode(value => value.getTime()) + +Route('/exampleB', Timestamp, Timestamp, (value) => { + return value // value observed as Date +}) +``` + + + +## Rest Types + +Revision 0.31.0 updates the Rest type to support variadic tuple extraction from Union, Intersection and Tuple types. Previously the Rest type was limited to Tuple types only, but has been extended to other types to allow uniform remapping without having to extract types from specific schema representations. + +The following remaps a Tuple into a Union. + +```typescript +const T = Type.Tuple([ // const T = { + Type.String(), // type: 'array', + Type.Number() // items: [ +]) // { type: 'string' }, + // { type: 'number' } + // ], + // additionalItems: false, + // minItems: 2, + // maxItems: 2, + // } + +const R = Type.Rest(T) // const R = [ + // { type: 'string' }, + // { type: 'number' } + // ] + +const U = Type.Union(R) // const U = { + // anyOf: [ + // { type: 'string' }, + // { type: 'number' } + // ] + // } +``` +This type can be used to remap Intersect a Composite + +```typescript +const I = Type.Intersect([ // const I = { + Type.Object({ x: Type.Number() }), // allOf: [{ + Type.Object({ y: Type.Number() }) // type: 'object', +]) // required: ['x'], + // properties: { + // x: { type: 'number' } + // } + // }, { + // type: 'object', + // required: ['y'], + // properties: { + // y: { type: 'number' } + // } + // }] + // } + +const C = Type.Composite(Type.Rest(I)) // const C = { + // type: 'object', + // required: ['x', 'y'], + // properties: { + // 'x': { type: 'number' }, + // 'y': { type: 'number' } + // } + // } +``` + + + +## Record Key + +Revision 0.31.0 updates the inference strategy for Record types and generalizes RecordKey to TSchema. This update aims to help Record types compose better when used with generic functions. The update also removes the overloaded Record factory methods, opting for a full conditional inference path. It also removes the `RecordKey` type which would type error when used with Record overloads. The return type of Record will be TNever if passing an invalid key. Valid Record key types include TNumber, TString, TInteger, TTemplateLiteral, TLiteralString, TLiteralNumber and TUnion. + +```typescript +// 0.30.0 +// +import { RecordKey, TSchema } from '@sinclair/typebox' + +function StrictRecord(K: K, T: T) { + return Type.Record(K, T, { additionalProperties: false }) // Error: RecordKey unresolvable to overload +} +// 0.31.0 +// +import { TSchema } from '@sinclair/typebox' + +function StrictRecord(K: K, T: T) { + return Type.Record(K, T, { additionalProperties: false }) // Ok: dynamically mapped +} + +const A = StrictRecord(Type.String(), Type.Null()) // const A: TRecord + +const B = StrictRecord(Type.Literal('A'), Type.Null()) // const B: TObject<{ A: TNull }> + +const C = StrictRecord(Type.BigInt(), Type.Null()) // const C: TNever +``` + + + +## TypeBoxError + +Revision 0.31.0 updates all errors thrown by TypeBox to extend the sub type `TypeBoxError`. This can be used to help narrow down the source of errors in `try/catch` blocks. + +```typescript +import { Type, TypeBoxError } from '@sinclair/typebox' +import { Value } from '@sinclair/typebox/value' + +try { + const A = Value.Decode(Type.Number(), 'hello') +} catch(error) { + if(error instanceof TypeBoxError) { + // typebox threw this error + } +} +``` + + + +## TypeSystemErrorFunction + +Revision 0.31.0 adds functionality to remap error messages with the TypeSystemErrorFunction. This function is invoked whenever a validation error is generated in TypeBox. The following is an example of a custom TypeSystemErrorFunction using some of the messages TypeBox generates by default. TypeBox also provides the DefaultErrorFunction which can be used for fallthrough cases. + +```typescript +import { TypeSystemErrorFunction, DefaultErrorFunction } from '@sinclair/typebox/system' + +// Example CustomErrorFunction +export function CustomErrorFunction(schema: Types.TSchema, errorType: ValueErrorType) { + switch (errorType) { + case ValueErrorType.ArrayContains: + return 'Expected array to contain at least one matching value' + case ValueErrorType.ArrayMaxContains: + return `Expected array to contain no more than ${schema.maxContains} matching values` + case ValueErrorType.ArrayMinContains: + return `Expected array to contain at least ${schema.minContains} matching values` + ... + default: return DefaultErrorFunction(schema, errorType) + } +} +// Sets the CustomErrorFunction +TypeSystemErrorFunction.Set(CustomErrorFunction) +``` +It is possible to call `.Set()` on the TypeSystemErrorFunction module prior to each call to `.Errors()`. This can be useful for applications that require i18n support in their validation pipelines. + + + +## Reduce Package Size + +Revision 0.31.0 completes a full sweep of code optimizations and modularization to reduce package bundle size. The following table shows the bundle sizes inclusive of the new 0.31.0 functionality against 0.30.0. + +```typescript +// Revision 0.30.0 +// +┌──────────────────────┬────────────┬────────────┬─────────────┐ +│ (index) │ Compiled │ Minified │ Compression │ +├──────────────────────┼────────────┼────────────┼─────────────┤ +│ typebox/compiler │ '131.4 kb' │ ' 59.4 kb' │ '2.21 x' │ +│ typebox/errors │ '113.6 kb' │ ' 50.9 kb' │ '2.23 x' │ +│ typebox/system │ ' 78.5 kb' │ ' 32.5 kb' │ '2.42 x' │ +│ typebox/value │ '182.8 kb' │ ' 80.0 kb' │ '2.28 x' │ +│ typebox │ ' 77.4 kb' │ ' 32.0 kb' │ '2.42 x' │ +└──────────────────────┴────────────┴────────────┴─────────────┘ + +// Revision 0.31.0 +// +┌──────────────────────┬────────────┬────────────┬─────────────┐ +│ (index) │ Compiled │ Minified │ Compression │ +├──────────────────────┼────────────┼────────────┼─────────────┤ +│ typebox/compiler │ '149.5 kb' │ ' 66.1 kb' │ '2.26 x' │ +│ typebox/errors │ '112.1 kb' │ ' 49.4 kb' │ '2.27 x' │ +│ typebox/system │ ' 83.2 kb' │ ' 37.1 kb' │ '2.24 x' │ +│ typebox/value │ '191.1 kb' │ ' 82.7 kb' │ '2.31 x' │ +│ typebox │ ' 73.0 kb' │ ' 31.9 kb' │ '2.29 x' │ +└──────────────────────┴────────────┴────────────┴─────────────┘ +``` + +Additional code reductions may not be possible without implicating code maintainability. The `typebox` module may however be broken down into sub modules in later revisions to further bolster modularity, but is retained as a single file on this revision for historical reasons (not necessarily technical ones). + + + +## JsonTypeBuilder and JavaScriptTypeBuilder + +Revision 0.31.0 renames the `StandardTypeBuilder` and `ExtendedTypeBuilder` to `JsonTypeBuilder` and `JavaScriptTypeBuilder` respectively. Applications that extend TypeBox's TypeBuilders will need to update to these names. + +```typescript +// 0.30.0 +// +export class ApplicationTypeBuilder extends ExtendedTypeBuilder {} + +// 0.31.0 +// +export class ApplicationTypeBuilder extends JavaScriptTypeBuilder {} +``` +These builders also update the jsdoc comment to `[Json]` and `[JavaScript]` inline with this new naming convention. + + + +## TypeSystemPolicy + +Revision 0.31.0 moves the `TypeSystem.Policy` configurations into a new type named `TypeSystemPolicy`. This change was done to unify internal policy checks used by the Value and Error modules during bundle size optimization; as well as to keep policy configurations contextually separate from the Type and Format API on the TypeSystem module. + +```typescript +// Revision 0.30.0 +// +import { TypeSystem } from '@sinclair/typebox/system' + +TypeSystem.AllowNaN = true + +// Revision 0.31.0 +// +import { TypeSystemPolicy } from '@sinclair/typebox/system' + +TypeSystemPolicy.AllowNaN = true + +TypeSystemPolicy.IsNumberLike(NaN) // true +``` \ No newline at end of file diff --git a/changelog/0.32.0.md b/changelog/0.32.0.md new file mode 100644 index 000000000..d28eb5e2d --- /dev/null +++ b/changelog/0.32.0.md @@ -0,0 +1,514 @@ +### 0.32.0 + +--- + +### Revision Updates + +- [Revision 0.32.35](https://github.com/sinclairzx81/typebox/pull/914) Support Any for Record keys, Revert error message on required property, Fix order dependency for Union Convert. +- [Revision 0.32.34](https://github.com/sinclairzx81/typebox/pull/914) Fix template literal generation for template literals embedded within template literals. +- [Revision 0.32.33](https://github.com/sinclairzx81/typebox/pull/905) Pin ESM compiler target to ES2020. +- [Revision 0.32.32](https://github.com/sinclairzx81/typebox/pull/898) Fix for Enum properties when used with Mapped types. +- [Revision 0.32.31](https://github.com/sinclairzx81/typebox/pull/881) Fix for Cast. Dereference Union variants before scoring. +- [Revision 0.32.30](https://github.com/sinclairzx81/typebox/pull/868) Support null object prototypes for Encode/Decode. +- [Revision 0.32.29](https://github.com/sinclairzx81/typebox/pull/862) Key derive optimization to improve Intersect Encode/Decode performance. +- [Revision 0.32.28](https://github.com/sinclairzx81/typebox/pull/861) Fix for TransformEncode introduced with 0.32.24, 0.32.25 optimizations. +- [Revision 0.32.27](https://github.com/sinclairzx81/typebox/pull/854) Support for esm.sh and general build tooling updates. +- [Revision 0.32.26](https://github.com/sinclairzx81/typebox/pull/851) Optimization for number checks, use Number.isFinite(x) over typeof `number`. +- [Revision 0.32.25](https://github.com/sinclairzx81/typebox/pull/849) Optimizations for type builder to improve schema creation performance for computed types. +- [Revision 0.32.24](https://github.com/sinclairzx81/typebox/pull/848) Optimizations for Convert to avoid unnecessary object initialization and cloning. +- [Revision 0.32.22](https://github.com/sinclairzx81/typebox/pull/840) Add Support for Optional and Readonly Function and Constructor Arguments. +- [Revision 0.32.21](https://github.com/sinclairzx81/typebox/pull/836) Refactor Array Conversion logic. Discard TNever on TComposite. +- [Revision 0.32.20](https://github.com/sinclairzx81/typebox/pull/810) Fix compiler regression (TS 5.3.3 -> 5.4.2) generating Diff declaration structures. +- [Revision 0.32.19](https://github.com/sinclairzx81/typebox/pull/805) Revert Union Convert logic added on 0.32.16. +- [Revision 0.32.18](https://github.com/sinclairzx81/typebox/pull/801) Add explicit return type on TypeSystem.Type. +- [Revision 0.32.17](https://github.com/sinclairzx81/typebox/pull/799) Detect ambiguous inference for StaticDecode when inferring as any. +- [Revision 0.32.16](https://github.com/sinclairzx81/typebox/pull/791) Enhance Composite, Mapped, Indexed and Transform types. Intersect and Union Convert updates, Include Path in Validation Error. +- [Revision 0.32.15](https://github.com/sinclairzx81/typebox/pull/774) Additional internal guards for Type Arrays, Map and Set structures. +- [Revision 0.32.14](https://github.com/sinclairzx81/typebox/pull/753) Use barrel exports for submodules. +- [Revision 0.32.13](https://github.com/sinclairzx81/typebox/pull/744) Add minLength and maxLength constraint for RegExp +- [Revision 0.32.12](https://github.com/sinclairzx81/typebox/pull/740) Fix option assignment on Record types. +- [Revision 0.32.11](https://github.com/sinclairzx81/typebox/pull/738) Optimize Extract, Exclude. Overloads for Template Literal +- [Revision 0.32.10](https://github.com/sinclairzx81/typebox/pull/734) Export additional type infrastructure for Partial and Required +- [Revision 0.32.9](https://github.com/sinclairzx81/typebox/pull/731) Generalize Composite to accept schematics of type TSchema[] +- [Revision 0.32.8](https://github.com/sinclairzx81/typebox/pull/728) Ensure schema `default` annotation is cloned on Create. +- [Revision 0.32.7](https://github.com/sinclairzx81/typebox/pull/727) Ensure schema `default` annotation is cloned on Default. +- [Revision 0.32.6](https://github.com/sinclairzx81/typebox/pull/724) Export additional type infrastructure for mapping types +- [Revision 0.32.5](https://github.com/sinclairzx81/typebox/pull/718) Update licence year span for 2024 +- [Revision 0.32.4](https://github.com/sinclairzx81/typebox/pull/708) Ensure ErrorFunctionParameter type is exported +- [Revision 0.32.3](https://github.com/sinclairzx81/typebox/pull/703) Simplify Record Static Type +- [Revision 0.32.1](https://github.com/sinclairzx81/typebox/pull/701) Specify default exports for Web Pack + + +## [0.32.0](https://www.npmjs.com/package/@sinclair/typebox/v/0.32.0) + +## Overview + +Revision 0.32.0 adds support for ESM and carries out the work necessary to fully modularize the TypeBox type system to enable selective type imports. This revision also adds three new types (Mapped, Const, and Deref), along with two new Value functions (Clean and Default) as well as many enhancements to existing types (Index, KeyOf, RegExp, Optional and Readonly). This revision also carries out many internal optimizations to enhance type inference across all types. + +This revision is a milestone revision for the TypeBox project. It has several breaking changes and requires a minor revision. + +## Contents + +- [Type Imports](#Type-Imports) +- [Value Function Import](#Value-Function-Imports) +- [CommonJS and ESM](#CommonJS-and-ESM) +- [Types](#Types) + - [Mapped Type](#Types-Mapped-Type) + - [Const Type](#Types-Const-Type) + - [Deref Type](#Types-Deref-Type) + - [RegExp Type](#Types-RegExp-Type) + - [Subtract Modifiers](#Types-Subtract-Modifiers) +- [Values](#Values) + - [Clean Function](#Values-Clean-Function) + - [Default Function](#Values-Default-Function) +- [Errors](#Errors) + - [Error Parameter](#Errors-Error-Parameter) +- [Optimizations](#Optimizations) + - [Bundle Size](#Optimizations-Bundle-Size) +- [Breaking](#Breaking) + - [Renamed Symbols](#Breaking-Renamed-Symbols) + - [TypeGuard Interface Change](#Breaking-TypeGuard-Interface-Change) + - [Value Submodule Imports](#Breaking-Value-Submodule-Imports) + - [Error Function](#Breaking-Error-Function) + - [RegEx](#Breaking-RegEx) + + + +### Type Imports + +Revision 0.32.0 adds the ability to import types individually. + +```typescript +import { Type, type Static } from '@sinclair/typebox' // classic - 37.0 kb minified + +import { Object, String, Number, type Static } from '@sinclair/typebox' // selective - 6.5 kb minified +``` + + + +### Value Function Imports + +Revision 0.32.0 adds the ability to import value functions from the `/value` module path. + +```typescript +import { Value } from '@sinclair/typebox/value' // classic - 61.5 kb minified + +import { Check } from '@sinclair/typebox/value' // selective - 18.2 kb minified +``` + +### CommonJS and ESM + + + +Revision 0.32.0 now publishes both CommonJS and ESM builds of TypeBox. Existing CommonJS users should not be impacted by the addition of ESM. For ESM users however, particularily those using bundlers, it's now possible to benefit from deep tree shake optimizations provided by modern bundler tooling. + + + +## Types + +Revision 0.32.0 adds three new types to the type system and makes enhancements to Readonly and Optional modifiers. + + + +### Mapped Type + +Revision 0.32.0 adds the Mapped type which replicates TS [Mapped Types](https://www.typescriptlang.org/docs/handbook/2/mapped-types.html) at runtime. The following shows the syntax comparison between TypeScript and TypeBox. + +#### TypeScript + +```typescript +type T = { + x: number, + y: number, + z: number +} + +type M = { [K in keyof T]: T[K] } // a mapped type +``` +#### TypeBox +```typescript +const T = Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number() +}) + +const M = Type.Mapped(Type.KeyOf(T), K => Type.Index(T, K)) // a mapped type +``` +Mapped types use a functional design to replicate the TypeScript feature. For users interested in this type, it may be helpful to use the [TypeBox Workbench](https://sinclairzx81.github.io/typebox-workbench/) which can generate runtime Mapped types from TypeScript syntax. + + + +### Const Type + +Revision 0.32.0 adds a new Const type that creates `readonly` types from object, array and primitive literal values. This type analogs the TypeScript `as const` syntax. The following shows general usage. + +```typescript +const A = Type.Const(1 as const) // const A: TLiteral<1> + +const B = Type.Const([1, 2, 3] as const) // const B: TReadonly, + // TLiteral<2>, + // TLiteral<3> + // ]>> + +const C = Type.Const({ // const C: TObject<{ + x: 1, // x: TReadonly>, + y: 2, // y: TReadonly>, + z: 3 // z: TReadonly>, +} as const) // }> +``` +Revision 0.32.0 continues support for TypeScript 4.0, and because of this, the `as const` syntax must be appended to each literal value passed to the Const type. When TypeBox ends support for 4.0, updates will be made to this type to make use of [Const Type Parameters](https://devblogs.microsoft.com/typescript/announcing-typescript-5-0/#const-type-parameters). This update will enable TypeBox to correctly infer the readonly literal type without the need for `as const`. + + + + +### Deref Type + +Revision 0.32.0 adds a new Type.Deref type which can be used to dereference type schematics. + +```typescript +const Vector = Type.Object({ // const Vector = { + x: Type.Number(), // type: 'object', + y: Type.Number(), // required: ['x', 'y', 'z'], +}, { $id: 'Vector' }) // properties: { + // x: { type: 'number' }, + // y: { type: 'number' } + // }, + // $id: 'Vector' + // } + +const VectorRef = Type.Ref(Vector) // const VectorRef = { + // $ref: 'Vector' + // } +// ... Embedded Reference Type + +const Vertex = Type.Object({ // const Vertex = { + position: VectorRef, // type: 'object', + texcoord: VectorRef, // required: ['position', 'texcoord'], +}) // properties: { + // position: { $ref: 'Vector' }, + // texcoord: { $ref: 'Vector' } + // } + // } + +// ... Dereferenced Embedded Reference Type + +const VertexDeref = Type.Deref(Vertex, [Vector]) // const VertexDeref = { + // type: 'object', + // required: ['position', 'texcoord'], + // properties: { + // position: { + // type: 'object', + // required: ['x', 'y', 'z'], + // properties: { + // x: { type: 'number' }, + // y: { type: 'number' } + // } + // }, + // texcoord: { + // type: 'object', + // required: ['x', 'y', 'z'], + // properties: { + // x: { type: 'number' }, + // y: { type: 'number' } + // } + // } + // } + // } +``` +The addition of Deref was prompted by issues composing reference types with mapping types (such as Partial, Required, Pick and Omit) which is generally not supported. Prior to Revision 0.32.0, there was some expectation for users to maintain and dereference types manually. In 0.32.0, users will still need to maintain references, but Deref will offer a more convenient mechanism to normalize reference types prior to composition. + + + + +### RegExp Type + +Revision 0.32.0 updates RegExp to support the full ECMA 262 regular expression syntax. In previous revisions, this type had been expressed as an alias for `TString` with a `pattern` to try ensure compliance with the [regular expression](https://json-schema.org/understanding-json-schema/reference/regular_expressions) subset supported by Json Schema. In Revision 0.32.0, RegExp is given a new type representation unto itself (named `TRegExp`) which houses both `source` and `flags` properties used to reconstruct a JavaScript regular expression object, making it properly distinct from `TString` and fully supportive of UTF-16. + +```typescript +// Case Insensitive + +const T = Type.RegExp(/abc/i) // const T = { + // type: 'RegExp', + // source: 'abc', + // flags: 'i' + // } + +type T = Static // type T = string + +Value.Check(T, 'abc') // ok +Value.Check(T, 'ABC') // ok + +// Extended Syntax + +const E = Type.RegExp(/|\p{Extended_Pictographic}/gu) + +Value.Check(E, '♥️♦️♠️♣️') // ok - emoji supported +``` + +The RegExp type can be thought of as a more capable TemplateLiteral that can only reasonably infer as `string`. Additionally, the RegExp inference type of `string` is unique to the other `[JavaScript]` types in that it does not infer as it's named type. The updates to RegExp were prompted by the limitations with Json Schema expressions, and to provide better options for users requiring general Unicode validation support. For Json Schema compliance, the recommendation moving forward will be to use either String with pattern or TemplateLiteral. + +```typescript +const T = Type.String({ pattern: '^(a|b|c)$' }) // Json Schema compliant + +const T = Type.TemplateLiteral('${a|b|c}') // Json Schema compliant + +const T = Type.RegExp(/$(a|b|c)$/) // Non Json Schema compliant +``` + + + +### Subtract Modifier + +Revision 0.32.0 adds new overloads for Readonly and Optional modifiers that enable them to subtract (or remove) that modifier from a type. Both Readonly and Optional now accept an optional secondary boolean argument that if `false`, will remove the modifier. + +#### TypeScript +```typescript +type T = { + x?: number, + y?: number +} +type M = { [K in keyof T]-?: T[K] } // -? - subtract optional modifier +``` +#### TypeBox +```typescript +const T = Type.Object({ + x: Type.Optional(Type.Number()), + y: Type.Optional(Type.Number()) +}) +const M = Type.Mapped(Type.KeyOf(T), K => { + return Type.Optional(Type.Index(T, K), false) // false - subtract optional modifier +}) +``` +Subtractive modifiers are provided in support of the new Mapped type feature. + + + +## Values + +Revision 0.32.0 adds two new functions to the Value module. + + + +### Clean Function + +Revision 0.32.0 adds a new Clean function that can be used to omit any values unknown to the type. This function will work irrespective of if `additionalProperties` is specified on the type. The Clean function is intended to replicate the functionality of Ajv's `removeAdditional` configuration. + +```typescript +const T = Type.Object({ + x: Type.Number(), + y: Type.Number() +}) + +const X = Value.Clean(T, null) // const 'X = null + +const Y = Value.Clean(T, { x: 1 }) // const 'Y = { x: 1 } + +const Z = Value.Clean(T, { x: 1, y: 2, z: 3 }) // const 'Z = { x: 1, y: 2 } +``` + +Note: the Clean function does not check the validity of the value being cleaned, and does not provide assurances that the result will be valid. Its return value is `unknown` and should be checked before use. + + + +### Default Function + +Revision 0.32.0 adds a new Default function that can be used to add missing values if the type specifies a `default` annotation. This function is intended to replicate Ajv's `useDefaults` functionality. + +```typescript +const T = Type.Object({ + x: Type.Number({ default: 0 }), + y: Type.Number({ default: 0 }) +}) + +const X = Value.Default(T, null) // const 'X = null - non-enumerable + +const Y = Value.Default(T, { }) // const 'Y = { x: 0, y: 0 } + +const Z = Value.Default(T, { x: 1 }) // const 'Z = { x: 1, y: 0 } +``` + +The Default function does not check the validity of the value being defaulted, and does not provide assurances that the result will be valid. Its return value is `unknown` and should be checked before use. + + + +## Optimizations + +Following the work to modularize TypeBox's type system, additional optimizations were carried out across each submodule to only import dependent type infrastructure. This has led to some fairly significant reductions in output sizes across each submodule. The main TypeBox import has increased in size due in part to the new Mapped types feature and other associative types, however selective imports supported on this revision should offer options for users concerned about output bundle size. There will be contined work to optimize the new type system throughout 0.32.0 and subsequent revisions. + +The following shows the comparisons between 0.31.0 and 0.32.0. + + + +```typescript +// Revision 0.31.0 + +┌──────────────────────┬────────────┬────────────┬─────────────┐ +│ (index) │ Compiled │ Minified │ Compression │ +├──────────────────────┼────────────┼────────────┼─────────────┤ +│ typebox/compiler │ '163.6 kb' │ ' 71.6 kb' │ '2.28 x' │ +│ typebox/errors │ '113.3 kb' │ ' 50.1 kb' │ '2.26 x' │ +│ typebox/system │ ' 83.9 kb' │ ' 37.5 kb' │ '2.24 x' │ +│ typebox/value │ '191.1 kb' │ ' 82.3 kb' │ '2.32 x' │ +│ typebox │ ' 73.8 kb' │ ' 32.3 kb' │ '2.29 x' │ +└──────────────────────┴────────────┴────────────┴─────────────┘ + +// Revision 0.32.0 + +┌──────────────────────┬────────────┬────────────┬─────────────┐ +│ (index) │ Compiled │ Minified │ Compression │ +├──────────────────────┼────────────┼────────────┼─────────────┤ +│ typebox/compiler │ '120.6 kb' │ ' 52.9 kb' │ '2.28 x' │ +│ typebox/errors │ ' 55.7 kb' │ ' 25.5 kb' │ '2.19 x' │ +│ typebox/system │ ' 4.7 kb' │ ' 2.0 kb' │ '2.33 x' │ +│ typebox/value │ '146.2 kb' │ ' 62.0 kb' │ '2.36 x' │ +│ typebox │ ' 91.4 kb' │ ' 37.8 kb' │ '2.42 x' │ +└──────────────────────┴────────────┴────────────┴─────────────┘ +``` + + +## Errors + +Revision 0.32.0 makes some enhancements to errors. + + + +### Error Parameter + +Revision 0.32.0 updates TypeBox's ErrorFunction to accept an ErrorParameter that contains additional information regarding the cause of a validation error. In Revision 0.31.0, only `errorType` and `schema` were passed through to this function. In 0.32.0 the additional properties `value` and `path` are also passed through. This update was prompted by some users needing to be able to generate specific error messages derived from specific values or other associated information. + +The following shows the changes from 0.31.0 to 0.32.0. + +```typescript +// Revision 0.31.0 + +import { TypeSystemErrorFunction } from '@sinclair/typebox/system' + +TypeSystemErrorFunction.Set((schema, errorType) => { + + return 'oh no, an error!' +}) + +// Revision 0.32.0 + +import { SetErrorFunction } from '@sinclair/typebox/errors' + +SetErrorFunction(({ schema, errorType, path, value }) => { // as destructured object + + return 'oh no, an error!' +}) +``` +Note that Revision 0.32.0 does make a breaking interface change by moving the ErrorFunction from `/system` to `/errors`. See breaking changes for more information. + + + +## Breaking + +The following list the breaking changes in Revision 0.32.0. + + + + +### Renamed Symbols + +Revision 0.32.0 renames the `Optional`, `Required` and `Transform` symbols to `OptionalKind`, `RequiredKind` and `TransformKind`. This change was necessary to avoid conflicts with exported type functions. + +```typescript +// Revision 0.31.0 +import { Kind, Hint, Optional, Required, Transform } from '@sinclair/typebox' // these are symbols + +// Revision 0.32.0 +import { + Kind, Hint, OptionalKind, RequiredKind, TransformKind, // these are symbols + Optional, Required, Transform // these are type imports +} from '@sinclair/typebox' +``` + + + +### TypeGuard Interface Change + +Revision 0.32.0 has a breaking interface change on the TypeGuard utility where the `T` prefixed guard functions have been updated to use the `Is` prefix. This naming change is perhaps somewhat more sensible than the previous naming, however the update was largely prompted by TypeScript compiler issues where interface types (i.e. `TString`) where conflicting with the `TString` functions leading to breakage in CommonJS. + +```typescript +// Revision 0.31.0 + +import { TypeGuard, Kind } from '@sinclair/typebox' + +const R = TypeGuard.TString({ ... }) + +// Revision 0.32.0 + +import { TypeGuard } from '@sinclair/typebox' + +const R = TypeGuard.IsString({ ... }) +``` + + + +### Value Submodule Imports + +The value submodule function import paths are unfortunately no longer supported. Instead, these can be imported directly on the `/value` path. The need to break the submodule paths was mostly due to complexities configuring dual ESM and CommonJS publishing for the package, as well as retaining support for pre and post node16 module resolution (of which many complexities reside, both for Node as well as for TypeScript type module resolution) + +```typescript +// Revision 0.31.0 + +import { Check } from '@sinclair/typebox/value/check' + +// Revision 0.32.0 + +import { Check } from '@sinclair/typebox/value' +``` + + + +### Error Function + +The TypeSystemErrorFunction has been replaced with SetErrorFunction which can be imported on the `/errors` submodule. This change is generally a tidy up, and to reserve the `/system` submodule for type system policy configuration, as well as future Json Schema generation options (draft 2020-12) + +```typescript +// Revision 0.31.0 + +import { TypeSystemErrorFunction, ValueErrorType, DefaultErrorFunction } from '@sinclair/typebox/system' + +TypeSystemErrorFunction.Set((schema, errorType) => { // i18n override + switch(errorType) { + /* en-US */ case ValueErrorType.String: return 'Expected string' + /* fr-FR */ case ValueErrorType.Number: return 'Nombre attendu' + /* ko-KR */ case ValueErrorType.Boolean: return '예상 부울' + /* en-US */ default: return DefaultErrorFunction(schema, errorType) + } +}) + +// Revision 0.32.0 + +import { SetErrorFunction, ValueErrorType, DefaultErrorFunction } from '@sinclair/typebox/errors' + +SetErrorFunction((error) => { // i18n override + switch(error.errorType) { + /* en-US */ case ValueErrorType.String: return 'Expected string' + /* fr-FR */ case ValueErrorType.Number: return 'Nombre attendu' + /* ko-KR */ case ValueErrorType.Boolean: return '예상 부울' + /* en-US */ default: return DefaultErrorFunction(error) + } +}) +``` + + + +### RegEx + +This RegEx function was flagged for deprecation on 0.30.0. It has been removed on Revision 0.32.0. Use the Type.RegExp type, or Type.String with a pattern to remain compatible with the Json Schema specification. + +```typescript +// Revision 0.31.0 + +const T = Type.RegEx(/abc/) // deprecation warning + +// Revision 0.32.0 + +const A = Type.RegExp(/abc/) // JavaScript Type + +const B = Type.String({ pattern: /abc/.source }) // Json Type +``` \ No newline at end of file diff --git a/changelog/0.33.0.md b/changelog/0.33.0.md new file mode 100644 index 000000000..c8c44a896 --- /dev/null +++ b/changelog/0.33.0.md @@ -0,0 +1,59 @@ +### 0.33.0 + +--- + +### Revision Updates + +- [Revision 0.33.22](https://github.com/sinclairzx81/typebox/pull/1065) + - Rename TypeScript parsing infrastructure from `/parse` to `/syntax`. Remove Parse API from top level import. +- [Revision 0.33.21](https://github.com/sinclairzx81/typebox/pull/1064) + - [1063](https://github.com/sinclairzx81/typebox/issues/1063) Hotfix to resolve variable shadowing on Object (Parser Runtime) +- [Revision 0.33.20](https://github.com/sinclairzx81/typebox/pull/1062) + - Add TypeScript Parsing Infrastructure. Add Parse API to top level import. +- [Revision 0.33.19](https://github.com/sinclairzx81/typebox/pull/1061) + - Preemptive fix for TypeScript 5.8.0-dev (Type Fix for Immutable Function) +- [Revision 0.33.18](https://github.com/sinclairzx81/typebox/pull/1060) + - [1052](https://github.com/sinclairzx81/typebox/pull/1052) Export the Encode | Decode functions directly. Refactoring on Value submodule. + - [1057](https://github.com/sinclairzx81/typebox/pull/1057) Export Object with var declaration to prevent global shadowing. Related Babel [Issue](https://github.com/babel/babel/issues/16943). +- [Revision 0.33.17](https://github.com/sinclairzx81/typebox/pull/1042) + - [1041](https://github.com/sinclairzx81/typebox/issues/1041) Avoid Exponentiation operator on Value.Hash +- [Revision 0.33.16](https://github.com/sinclairzx81/typebox/pull/1015) + - [1015](https://github.com/sinclairzx81/typebox/issues/1015) Add sub error iterators to ValueError +- [Revision 0.33.15](https://github.com/sinclairzx81/typebox/pull/1025) + - [1024](https://github.com/sinclairzx81/typebox/issues/1024) Fix to correctly resolve default Dates +- [Revision 0.33.14](https://github.com/sinclairzx81/typebox/pull/1019) + - [1019](https://github.com/sinclairzx81/typebox/pull/1019) Converting Large Numbers to BigInt +- [Revision 0.33.13](https://github.com/sinclairzx81/typebox/pull/1011) + - [1010](https://github.com/sinclairzx81/typebox/pull/1011) Fixes Value.Parse fails with recursive types +- [Revision 0.33.12](https://github.com/sinclairzx81/typebox/pull/999) + - [998](https://github.com/sinclairzx81/typebox/issues/998) Avoid losing precision when converting to bigints +- [Revision 0.33.11](https://github.com/sinclairzx81/typebox/pull/994) + - [993](https://github.com/sinclairzx81/typebox/issues/993) Prevent mutation on union values during Convert +- [Revision 0.33.10](https://github.com/sinclairzx81/typebox/pull/991) + - [907](https://github.com/sinclairzx81/typebox/issues/907) Add package.json metadata to specify possible side effect modules +- [Revision 0.33.9](https://github.com/sinclairzx81/typebox/pull/984) + - [887](https://github.com/sinclairzx81/typebox/issues/887) Generate Nested Intersect Errors +- [Revision 0.33.8](https://github.com/sinclairzx81/typebox/pull/983) + - [982](https://github.com/sinclairzx81/typebox/issues/982) Prevent Intersect Transform Encode callback from being called twice + - [974](https://github.com/sinclairzx81/typebox/issues/974) Make strict the Encode and Decode return type + - [975](https://github.com/sinclairzx81/typebox/issues/975) Support default annotation being assigned Functions for lazy value initialization on Create + - [980](https://github.com/sinclairzx81/typebox/issues/980) Enable Mapping types to override user defined options from source type + - [976](https://github.com/sinclairzx81/typebox/issues/976) Support Constraint Copy for Pick, Omit (inline with Partial / Required) (Trialing Implementation) + - Flag Strict For Deprecation +- [Revision 0.33.7](https://github.com/sinclairzx81/typebox/pull/964) + - Additional updates to improve Default for enumerable objects. +- [Revision 0.33.6](https://github.com/sinclairzx81/typebox/pull/963) + - Add object traversal path for Default. Ensure enumerable objects are traversed. +- [Revision 0.33.5](https://github.com/sinclairzx81/typebox/pull/959) + - Provide better support for transforming properties with optional modifiers. +- [Revision 0.33.4](https://github.com/sinclairzx81/typebox/pull/953) + - Add Assert and Parse value functions. Add defacto AssertError type. +- [Revision 0.33.3](https://github.com/sinclairzx81/typebox/pull/950) + - Optimize Value Diff algorithm. Update edit sequence to INSERT, UPDATE then DELETE. +- [Revision 0.33.2](https://github.com/sinclairzx81/typebox/pull/947) + - Ensure user defined schema options are retained on mapping types, Pick, Omit and Mapped. +- [Revision 0.33.1](https://github.com/sinclairzx81/typebox/pull/945) + - Apply mutability fix for Intrinsic and Not schematics (inline with Default) +- [Revision 0.33.0](https://github.com/sinclairzx81/typebox/pull/941) + - Add InstanceMode to enable Clone, Freeze and Default schema initialization options. Optimize for Default. + diff --git a/changelog/0.34.0.md b/changelog/0.34.0.md new file mode 100644 index 000000000..db216fb8e --- /dev/null +++ b/changelog/0.34.0.md @@ -0,0 +1,289 @@ +### 0.34.0 + +--- + +### Revision Updates +- [Revision 0.34.41](https://github.com/sinclairzx81/typebox/pull/1310) + - Disable Node10 Module Resolution | TS7 Deprecation Warning. +- [Revision 0.34.40](https://github.com/sinclairzx81/typebox/pull/1293) + - Use Uniform over Reciprocal weighting on Cast Union Select +- [Revision 0.34.39](https://github.com/sinclairzx81/typebox/pull/1296) + - Guard for Array in Object and Record conversion +- [Revision 0.34.38](https://github.com/sinclairzx81/typebox/pull/1282) + - Preserve exact type matches in Union conversion +- [Revision 0.34.37](https://github.com/sinclairzx81/typebox/pull/1278) + - Fix Support nested Union selection when scoring for Cast +- [Revision 0.34.36](https://github.com/sinclairzx81/typebox/pull/1276) + - Fix Record Intersect on Cast +- [Revision 0.34.35](https://github.com/sinclairzx81/typebox/pull/1265) + - Deep Assign on Intersect Cast +- [Revision 0.34.34](https://github.com/sinclairzx81/typebox/pull/1263) + - Support Inference of Ref inside Recursive inside Module +- [Revision 0.34.33](https://github.com/sinclairzx81/typebox/pull/1220) + - Hotfix: Correct Invalid Import Specifier +- [Revision 0.34.32](https://github.com/sinclairzx81/typebox/pull/1218) + - Accelerated | High Performance Syntax Parsing +- [Revision 0.34.31](https://github.com/sinclairzx81/typebox/pull/1209) + - Use Tail Call Optimized Inference for Records with Large Union Keys +- [Revision 0.34.30](https://github.com/sinclairzx81/typebox/pull/1198) + - Additional Syntax Parsing Optimizations +- [Revision 0.34.29](https://github.com/sinclairzx81/typebox/pull/1197) + - Syntax Parsing Optimizations +- [Revision 0.34.28](https://github.com/sinclairzx81/typebox/pull/1187) + - Add Cast to Configurable Parse Pipeline +- [Revision 0.34.27](https://github.com/sinclairzx81/typebox/pull/1182) + - [1178](https://github.com/sinclairzx81/typebox/issues/1178) Support Deep Referential Transform Inference Inside Modules +- [Revision 0.34.26](https://github.com/sinclairzx81/typebox/pull/1181) + - Internal: Use Parser Context Threading for Generic Arguments. +- [Revision 0.34.25](https://github.com/sinclairzx81/typebox/pull/1176) + - Evaluate Conditional Expression for Instatiated Object Types +- [Revision 0.34.24](https://github.com/sinclairzx81/typebox/pull/1175) + - Add Support For Generic Parameter Syntax +- [Revision 0.34.23](https://github.com/sinclairzx81/typebox/pull/1174) + - Inline Instantiate Type Logic Local to Instantiate Module +- [Revision 0.34.22](https://github.com/sinclairzx81/typebox/pull/1171) + - Intrinsic Types Number, String, Boolean, etc Passthrough on Required and Partial Mapping +- [Revision 0.34.21](https://github.com/sinclairzx81/typebox/pull/1168) + - Reimplement Computed Record Types +- [Revision 0.34.20](https://github.com/sinclairzx81/typebox/pull/1167) + - Hotfix: Disable Computed Record Types +- [Revision 0.34.19](https://github.com/sinclairzx81/typebox/pull/1166) + - Hotfix: Republished due to NPM error +- [Revision 0.34.18](https://github.com/sinclairzx81/typebox/pull/1164) + - Hotfix: Internal Remap Imports +- [Revision 0.34.17](https://github.com/sinclairzx81/typebox/pull/1162) + - Add Argument() and Instantiate() Types and Instancing via Syntax support. +- [Revision 0.34.16](https://github.com/sinclairzx81/typebox/pull/1156) + - Export TypeBox String Parsing Infrastructure +- [Revision 0.34.15](https://github.com/sinclairzx81/typebox/pull/1148) + - [1147](https://github.com/sinclairzx81/typebox/issues/1147) Fix incorrect truncation for integers that exceed 32-bit values in Value.Convert +- [Revision 0.34.14](https://github.com/sinclairzx81/typebox/pull/1140) + - [1139](https://github.com/sinclairzx81/typebox/issues/1139) Update TypeCompiler Check for Promise (use instanceof Promise over Thenable check) +- [Revision 0.34.13](https://github.com/sinclairzx81/typebox/pull/1124) + - Pre emptive fix for TypeScript 5.8.0-nightly to resolve symbol narrowing on Convert. +- [Revision 0.34.12](https://github.com/sinclairzx81/typebox/pull/1120) + - [1119](https://github.com/sinclairzx81/typebox/issues/1119) Fix for Mutate Object Comparison + - [1117](https://github.com/sinclairzx81/typebox/issues/1117) Re-Add Type.Recursive Documentation +- [Revision 0.34.11](https://github.com/sinclairzx81/typebox/pull/1110) + - Fix Compiler Emit for Deeply Referential Module Types +- [Revision 0.34.10](https://github.com/sinclairzx81/typebox/pull/1107) + - Fix Declaration Emit for Index and Mapped Types + - Fix Record Inference Presentation when Embedded in Modules + - Fix Record Mapping when using TImport as Key + - Add Encode to Parse Operation List +- [Revision 0.34.9](https://github.com/sinclairzx81/typebox/pull/1101) + - User Defined Parse Pipelines + - Access to Schema and References on TypeCheck +- [Revision 0.34.8](https://github.com/sinclairzx81/typebox/pull/1098) + - Fix for Computed Readonly and Optional Properties +- [Revision 0.34.7](https://github.com/sinclairzx81/typebox/pull/1093) + - Revert Ref(Schema) Signature with Deprecation Notice +- [Revision 0.34.6](https://github.com/sinclairzx81/typebox/pull/1090) + - Add Computed To Type and Kind Guards (IsSchema) +- [Revision 0.34.5](https://github.com/sinclairzx81/typebox/pull/1088) + - Record Types no longer TCompute for TRef Value Type (Modules) +- [Revision 0.34.4](https://github.com/sinclairzx81/typebox/pull/1085) + - Inference Path for Enum within Modules +- [Revision 0.34.3](https://github.com/sinclairzx81/typebox/pull/1083) + - Retain Array Elements on Value Default +- [Revision 0.34.2](https://github.com/sinclairzx81/typebox/pull/1082) + - Resolve import pathing issue introduced on 0.34.1 +- [Revision 0.34.1](https://github.com/sinclairzx81/typebox/pull/1080) + - Implement Computed Type Deref in Modules + +## [0.34.0](https://www.npmjs.com/package/@sinclair/typebox/v/0.34.0) + +## Overview + +Revision 0.34.0 represents a significant milestone for the TypeBox project. This update changes how TypeBox manages type references (Ref) and introduces a new Module type to support mutual recursion and self-referencing types. Additionally, it includes a new submodule for parsing TypeScript syntax directly into TypeBox types. + +Please note that this release includes breaking changes to Ref and some deprecations. These updates require a minor semver revision. + +## Contents + +- [Enhancements](#Enhancements) + - [Module Types](#Module-Types) + - [Syntax Types](#Syntax-Types) +- [Breaking Changes](#Breaking-Changes) + - [Ref](#Ref) + - [Deref](#Deref) + - [Strict](#Strict) + + + + +## Enhancements + +Below are the enhancements introduced in Version 0.34.0. + + + +### Module Types + +Revision 0.34.0 introduces a new type, called Module. Modules are represented as JSON Schema $def schematics, specifically designed to support both mutual and self-recursive types. This addition resolves a longstanding issue in TypeBox, where complex recursive structures often encountered "definition order" problems, making certain recursive structures difficult to represent cleanly. With Modules, you can now define schematics within a Module context, allowing them to be referenced within the type system through a separate inference operation. + +```typescript +// The following creates a circular recursive type. + +const Module = Type.Module({ + A: Type.Object({ + b: Type.Ref('B') // Ref B: + }), + B: Type.Object({ + c: Type.Ref('C') // Ref C: + }), + C: Type.Object({ + a: Type.Ref('A') // Ref A: + }), +}) + +// Module types must be imported before use. + +const A = Module.Import('A') // const A: TImport<{...}, 'A'> + +type A = Static // type A = { + // b: { + // c: { + // a: { + // b: ... + // } + // } + // } + // } +``` + + + +### Syntax Types + + +Revision 0.34.0 introduces a new submodule for parsing TypeScript syntax directly into TypeBox types, implemented both at runtime and within the type system. This feature was made possible through the development of a separate project, [ParseBox](https://github.com/sinclairzx81/parsebox) (MIT-licensed), which provides a symmetric runtime and type-level parsing infrastructure. + +As of 0.34.0, Syntax Types are available as an opt-in feature, with the parsing infrastructure adding approximately 10kb (minified) to the existing type builder. With further optimizations, this feature may be elevated to a top-level import in future updates to minimize bundling size. + +To use Syntax Types, import them from the `@sinclair/typebox/syntax` path. + +```typescript +import { Parse } from '@sinclair/typebox/syntax' + +// All primitive types are supported + +const A = Parse('string') // const A: TString +const B = Parse('number') // const B: TNumber +const C = Parse('boolean') // const C: TBoolean + +// ... Multiline parsing is supported (but comments are not) + +const T = Parse(`{ + x: number + y: number + z: number +}`) + + +// ... Parametertized parsing is supported +const O = Parse({ T }, `T & { w: number }`) // const O: TIntersect<[ + // TObject<{ + // x: TNumber, + // y: TNumber, + // z: TNumber, + // }>, + // TObject<{ + // w: TNumber + // }> + // ]> + +// ... Module parsing is also supported. + +const Math = Parse(`module Math { + export interface X { + x: number + } + export interface Y { + y: number + } + export interface Z { + z: number + } + export interface Vector extends X, Y, Z { + type: 'Vector' + } +}`) + +const Vector = Math.Import('Vector') +``` + +Runtime parsing performance should be quite good; however, static parsing performance could be improved. TypeScript will invoke the parser for each property accessed at design time. Ongoing efforts within the ParseBox project aim to optimize string parsing in TypeScript, with additional research underway into type-level caching strategies within the TypeScript compiler. Additional optimizations will be explored over the course of 0.34.x. + + + +## Breaking Changes + +The following are the breaking changes in Revision 0.34.0. + + + +### Ref + +Revision 0.34.0 introduces a breaking change to Ref, modifying its signature to accept only constant string values. Previously, Ref could accept an existing TypeBox type, provided it had an $id assigned. + +```typescript + +// Revision 0.33.0 + +const T = Type.String({ $id: 'T' }) + +const R = Type.Ref(T) + +type R = Static // type R = string + +// Revision 0.34.0 + +const T = Type.String({ $id: 'T' }) + +const R = Type.Ref('T') + +type R = Static // type R = unknown + +``` + +In Revision 0.34.0, the inferred type for Ref is now unknown. Implementations using the previous version of Ref can switch to Unsafe to type the reference to the target value. + +```typescript +// Revision 0.34.0 + +const T = Type.String({ $id: 'T' }) + +const R = Type.Unsafe>(Type.Ref('T')) + +type R = Static // type R = string +``` + + + +### Deref + +Revision 0.34.0 removes the Deref type, which was previously used to dereference schematics. Since the Ref signature has changed from TSchema to string, there is no longer a way to resolve reference types accurately. TypeBox may provide a prototype example of this type upon request. + + + +### Strict + +Revision 0.34.0 removes the Strict type from the Type Builder, which was deprecated in version 0.33.8. This type was introduced several years ago in response to a change in Ajv that prohibited unknown keywords. At that time, TypeBox used string property keys for `kind` and `modifier`, which required either Ajv configuration or the use of Strict. These properties have since been updated to Symbol properties, resolving the issues with Ajv. However, the Strict type remained due to some use in ecosystem projects, which has since reduced. + +For those who still need Strict, the recommended approach is to use the JSON stringify/parse method outlined in the deprecation notice. + +```typescript +/** + * @deprecated `[Json]` Omits compositing symbols from this schema. It is recommended + * to use the JSON parse/stringify to remove compositing symbols if needed. This + * is how Strict works internally. + * + * ```typescript + * JSON.parse(JSON.stringify(Type.String())) + * ``` + */ +export function Strict(schema: T): TStrict { + return JSON.parse(JSON.stringify(schema)) +} +``` \ No newline at end of file diff --git a/example/annotation/annotation.ts b/example/annotation/annotation.ts new file mode 100644 index 000000000..90b8f7a20 --- /dev/null +++ b/example/annotation/annotation.ts @@ -0,0 +1,148 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import * as Types from '@sinclair/typebox' + +// ------------------------------------------------------------------- +// Annotation +// +// Generates TypeScript Type Annotations from TypeBox types +// ------------------------------------------------------------------- +/** Generates TypeScript Type Annotations from TypeBox types */ +export namespace Annotation { + // ----------------------------------------------------------------- + // Escape + // ----------------------------------------------------------------- + function Escape(content: string) { + return content.replace(/'/g, "\\'") + } + // ----------------------------------------------------------------- + // Types + // ----------------------------------------------------------------- + function Intersect(schema: Types.TSchema[], references: Types.TSchema[]): string { + const [L, ...R] = schema + // prettier-ignore + return R.length === 0 + ? `${Visit(L, references)}` + : `${Visit(L, references)} & ${Intersect(R, references)}` + } + function Union(schema: Types.TSchema[], references: Types.TSchema[]): string { + const [L, ...R] = schema + // prettier-ignore + return R.length === 0 + ? `${Visit(L, references)}` + : `${Visit(L, references)} | ${Union(R, references)}` + } + function Tuple(schema: Types.TSchema[], references: Types.TSchema[]): string { + const [L, ...R] = schema + // prettier-ignore + return R.length > 0 + ? `${Visit(L, references)}, ${Tuple(R, references)}` + : `` + } + function Property(schema: Types.TProperties, K: string, references: Types.TSchema[]): string { + const TK = schema[K] + // prettier-ignore + return ( + Types.TypeGuard.IsOptional(TK) && Types.TypeGuard.IsReadonly(TK) ? `readonly ${K}?: ${Visit(TK, references)}` : + Types.TypeGuard.IsReadonly(TK) ? `readonly ${K}: ${Visit(TK, references)}` : + Types.TypeGuard.IsOptional(TK) ? `${K}?: ${Visit(TK, references)}` : + `${K}: ${Visit(TK, references)}` + ) + } + function Properties(schema: Types.TProperties, K: string[], references: Types.TSchema[]): string { + const [L, ...R] = K + // prettier-ignore + return R.length === 0 + ? `${Property(schema, L, references)}` + : `${Property(schema, L, references)}; ${Properties(schema, R, references)}` + } + function Parameters(schema: Types.TSchema[], I: number, references: Types.TSchema[]): string { + const [L, ...R] = schema + // prettier-ignore + return R.length === 0 + ? `param_${I}: ${Visit(L, references)}` + : `param_${I}: ${Visit(L, references)}, ${Parameters(R, I + 1, references)}` + } + function Literal(schema: Types.TLiteral, references: Types.TSchema[]): string { + return typeof schema.const === 'string' ? `'${Escape(schema.const)}'` : schema.const.toString() + } + function Record(schema: Types.TRecord, references: Types.TSchema[]): string { + // prettier-ignore + return ( + Types.PatternBooleanExact in schema.patternProperties ? `Record` : + Types.PatternNumberExact in schema.patternProperties ? `Record` : + Types.PatternStringExact in schema.patternProperties ? `Record` : + `{}` + ) + } + function TemplateLiteral(schema: Types.TTemplateLiteral, references: Types.TSchema[]) { + const E = Types.TemplateLiteralParseExact(schema.pattern) + if (!Types.IsTemplateLiteralExpressionFinite(E)) return 'string' + return [...Types.TemplateLiteralExpressionGenerate(E)].map((literal) => `'${Escape(literal)}'`).join(' | ') + } + function Visit(schema: Types.TSchema, references: Types.TSchema[]): string { + // prettier-ignore + return ( + Types.TypeGuard.IsAny(schema) ? 'any' : + Types.TypeGuard.IsArray(schema) ? `${Visit(schema.items, references)}[]` : + Types.TypeGuard.IsAsyncIterator(schema) ? `AsyncIterableIterator<${Visit(schema.items, references)}>` : + Types.TypeGuard.IsBigInt(schema) ? `bigint` : + Types.TypeGuard.IsBoolean(schema) ? `boolean` : + Types.TypeGuard.IsConstructor(schema) ? `new (${Parameters(schema.parameter, 0, references)}) => ${Visit(schema.returns, references)}` : + Types.TypeGuard.IsDate(schema) ? 'Date' : + Types.TypeGuard.IsFunction(schema) ? `(${Parameters(schema.parameters, 0, references)}) => ${Visit(schema.returns, references)}` : + Types.TypeGuard.IsInteger(schema) ? 'number' : + Types.TypeGuard.IsIntersect(schema) ? `(${Intersect(schema.allOf, references)})` : + Types.TypeGuard.IsIterator(schema) ? `IterableIterator<${Visit(schema.items, references)}>` : + Types.TypeGuard.IsLiteral(schema) ? `${Literal(schema, references)}` : + Types.TypeGuard.IsNever(schema) ? `never` : + Types.TypeGuard.IsNull(schema) ? `null` : + Types.TypeGuard.IsNot(schema) ? 'unknown' : + Types.TypeGuard.IsNumber(schema) ? 'number' : + Types.TypeGuard.IsObject(schema) ? `{ ${Properties(schema.properties, Object.getOwnPropertyNames(schema.properties), references)} }` : + Types.TypeGuard.IsPromise(schema) ? `Promise<${Visit(schema.item, references)}>` : + Types.TypeGuard.IsRecord(schema) ? `${Record(schema, references)}` : + Types.TypeGuard.IsRef(schema) ? `${Visit(Types.Type.Deref(schema, references), references)}` : + Types.TypeGuard.IsString(schema) ? 'string' : + Types.TypeGuard.IsSymbol(schema) ? 'symbol' : + Types.TypeGuard.IsTemplateLiteral(schema) ? `${TemplateLiteral(schema, references)}` : + Types.TypeGuard.IsThis(schema) ? 'unknown' : // requires named interface + Types.TypeGuard.IsTuple(schema) ? `[${Tuple(schema.items || [], references)}]` : + Types.TypeGuard.IsUint8Array(schema) ? `Uint8Array` : + Types.TypeGuard.IsUndefined(schema) ? 'undefined' : + Types.TypeGuard.IsUnion(schema) ? `${Union(schema.anyOf, references)}` : + Types.TypeGuard.IsVoid(schema) ? `void` : + 'unknown' + ) + } + /** Generates a TypeScript annotation for the given schema */ + export function Code(schema: Types.TSchema, references: Types.TSchema[] = []): string { + return Visit(schema, references) + } +} \ No newline at end of file diff --git a/example/annotation/index.ts b/example/annotation/index.ts new file mode 100644 index 000000000..34ccf2617 --- /dev/null +++ b/example/annotation/index.ts @@ -0,0 +1 @@ +export * from './annotation' \ No newline at end of file diff --git a/example/collections/array.ts b/example/collections/array.ts new file mode 100644 index 000000000..0549cda89 --- /dev/null +++ b/example/collections/array.ts @@ -0,0 +1,350 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/collections + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { TypeCheck, TypeCompiler, ValueError } from '@sinclair/typebox/compiler' +import { TSchema, Static, TypeBoxError } from '@sinclair/typebox' +import { Value } from '@sinclair/typebox/value' + +// ---------------------------------------------------------------- +// TypeArrayError +// ---------------------------------------------------------------- +export class TypeArrayError extends TypeBoxError { + constructor(message: string) { + super(`${message}`) + } +} +export class TypeArrayLengthError extends TypeBoxError { + constructor() { + super('arrayLength not a number') + } +} +// ---------------------------------------------------------------- +// TypeArray +// ---------------------------------------------------------------- +export class TypeArray implements Iterable> { + readonly #typeCheck: TypeCheck + readonly #values: Static[] + constructor(schema: T, arrayLength: number = 0) { + if (typeof arrayLength !== 'number') throw new TypeArrayLengthError() + this.#typeCheck = TypeCompiler.Compile(schema) + this.#values = new Array(arrayLength) + for (let i = 0; i < arrayLength; i++) { + this.#values[i] = Value.Create(schema) + } + } + // --------------------------------------------------- + // Indexer + // --------------------------------------------------- + /** Sets the value at the given index */ + public set(index: number, item: Static): void { + this.#assertIndexInBounds(index) + this.#assertItem(index, item) + this.#values[index] = item + } + // --------------------------------------------------- + // Array + // --------------------------------------------------- + /** Iterator for values in this array */ + public [Symbol.iterator](): IterableIterator> { + return this.#values[Symbol.iterator]() as IterableIterator + } + /** Gets the value at the given index */ + public at(index: number): Static { + this.#assertIndexInBounds(index) + return this.#values[index] as T + } + /** + * Gets the length of the array. This is a number one higher than the highest index in the array. + */ + public get length(): number { + return this.#values.length + } + /** + * Returns a string representation of an array. + */ + public toString(): string { + return this.#values.toString() + } + /** + * Returns a string representation of an array. The elements are converted to string using their toLocaleString methods. + */ + public toLocaleString(): string { + return this.#values.toLocaleString() + } + /** + * Removes the last element from an array and returns it. + * If the array is empty, undefined is returned and the array is not modified. + */ + public pop(): Static | undefined { + return this.#values.pop() + } + /** + * Appends new elements to the end of an array, and returns the new length of the array. + * @param items New elements to add to the array. + */ + public push(...items: Static[]): number { + this.#assertItems(items) + return this.#values.push(...items) + } + /** + * Combines two or more arrays. + * This method returns a new array without modifying any existing arrays. + * @param items Additional arrays and/or items to add to the end of the array. + */ + public concat(...items: ConcatArray>[]): Static[] + /** + * Combines two or more arrays. + * This method returns a new array without modifying any existing arrays. + * @param items Additional arrays and/or items to add to the end of the array. + */ + public concat(...items: (T | ConcatArray>)[]): Static[] { + this.#assertItems(items) + return this.#values.concat(...items) as Static[] + } + /** + * Adds all the elements of an array into a string, separated by the specified separator string. + * @param separator A string used to separate one element of the array from the next in the resulting string. If omitted, the array elements are separated with a comma. + */ + public join(separator?: string): string { + return this.#values.join(separator) + } + /** + * Reverses the elements in an array in place. + * This method mutates the array and returns a reference to the same array. + */ + public reverse(): Static[] { + return this.#values.reverse() as Static[] + } + /** + * Removes the first element from an array and returns it. + * If the array is empty, undefined is returned and the array is not modified. + */ + public shift(): Static | undefined { + return this.#values.shift() as Static | undefined + } + /** + * Returns a copy of a section of an array. + * For both start and end, a negative index can be used to indicate an offset from the end of the array. + * For example, -2 refers to the second to last element of the array. + * @param start The beginning index of the specified portion of the array. + * If start is undefined, then the slice begins at index 0. + * @param end The end index of the specified portion of the array. This is exclusive of the element at the index 'end'. + * If end is undefined, then the slice extends to the end of the array. + */ + public slice(start?: number, end?: number): Static[] { + return this.#values.slice(start, end) as Static[] + } + /** + * Sorts an array in place. + * This method mutates the array and returns a reference to the same array. + * @param compareFn Function used to determine the order of the elements. It is expected to return + * a negative value if the first argument is less than the second argument, zero if they're equal, and a positive + * value otherwise. If omitted, the elements are sorted in ascending, ASCII character order. + * ```ts + * [11,2,22,1].sort((a, b) => a - b) + * ``` + */ + public sort(compareFn?: (a: Static, b: Static) => number): this { + this.#values.sort(compareFn as any) + return this + } + /** + * Removes elements from an array and, if necessary, inserts new elements in their place, returning the deleted elements. + * @param start The zero-based location in the array from which to start removing elements. + * @param deleteCount The number of elements to remove. + * @returns An array containing the elements that were deleted. + */ + public splice(start: number, deleteCount?: number): Static[] + /** + * Removes elements from an array and, if necessary, inserts new elements in their place, returning the deleted elements. + * @param start The zero-based location in the array from which to start removing elements. + * @param deleteCount The number of elements to remove. + * @param items Elements to insert into the array in place of the deleted elements. + * @returns An array containing the elements that were deleted. + */ + public splice(start: number, deleteCount: number, ...items: Static[]): Static[] { + this.#assertItems(items) + return this.#values.splice(start, deleteCount, items) as Static[] + } + /** + * Inserts new elements at the start of an array, and returns the new length of the array. + * @param items Elements to insert at the start of the array. + */ + public unshift(...items: Static[]): number { + this.#assertItems(items) + return this.#values.unshift(items) + } + /** + * Returns the index of the first occurrence of a value in an array, or -1 if it is not present. + * @param searchElement The value to locate in the array. + * @param fromIndex The array index at which to begin the search. If fromIndex is omitted, the search starts at index 0. + */ + public indexOf(searchElement: Static, fromIndex?: number): number { + return this.#values.indexOf(searchElement, fromIndex) + } + /** + * Returns the index of the last occurrence of a specified value in an array, or -1 if it is not present. + * @param searchElement The value to locate in the array. + * @param fromIndex The array index at which to begin searching backward. If fromIndex is omitted, the search starts at the last index in the array. + */ + public lastIndexOf(searchElement: Static, fromIndex?: number): number { + return this.#values.lastIndexOf(searchElement, fromIndex) + } + /** + * Determines whether all the members of an array satisfy the specified test. + * @param predicate A function that accepts up to three arguments. The every method calls + * the predicate function for each element in the array until the predicate returns a value + * which is coercible to the Boolean value false, or until the end of the array. + * @param thisArg An object to which the this keyword can refer in the predicate function. + * If thisArg is omitted, undefined is used as the this value. + */ + public every(predicate: (value: Static, index: number, array: Static[]) => value is Static, thisArg?: any): this is Static[] + /** + * Determines whether all the members of an array satisfy the specified test. + * @param predicate A function that accepts up to three arguments. The every method calls + * the predicate function for each element in the array until the predicate returns a value + * which is coercible to the Boolean value false, or until the end of the array. + * @param thisArg An object to which the this keyword can refer in the predicate function. + * If thisArg is omitted, undefined is used as the this value. + */ + public every(predicate: (value: Static, index: number, array: Static[]) => unknown, thisArg?: any): boolean { + return this.#values.every(predicate as any, thisArg) + } + /** + * Determines whether the specified callback function returns true for any element of an array. + * @param predicate A function that accepts up to three arguments. The some method calls + * the predicate function for each element in the array until the predicate returns a value + * which is coercible to the Boolean value true, or until the end of the array. + * @param thisArg An object to which the this keyword can refer in the predicate function. + * If thisArg is omitted, undefined is used as the this value. + */ + public some(predicate: (value: Static, index: number, array: T[]) => unknown, thisArg?: any): boolean { + return this.#values.some(predicate as any, thisArg) + } + /** + * Performs the specified action for each element in an array. + * @param callbackfn A function that accepts up to three arguments. forEach calls the callbackfn function one time for each element in the array. + * @param thisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value. + */ + public forEach(callbackfn: (value: Static, index: number, array: Static[]) => void, thisArg?: any): void { + return this.#values.forEach(callbackfn as any, thisArg) + } + /** + * Calls a defined callback function on each element of an array, and returns an array that contains the results. + * @param callbackfn A function that accepts up to three arguments. The map method calls the callbackfn function one time for each element in the array. + * @param thisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value. + */ + public map(callbackfn: (value: Static, index: number, array: Static[]) => U, thisArg?: any): U[] { + return this.#values.map(callbackfn as any, thisArg) + } + /** + * Returns the elements of an array that meet the condition specified in a callback function. + * @param predicate A function that accepts up to three arguments. The filter method calls the predicate function one time for each element in the array. + * @param thisArg An object to which the this keyword can refer in the predicate function. If thisArg is omitted, undefined is used as the this value. + */ + public filter(predicate: (value: Static, index: number, array: Static[]) => value is Static, thisArg?: any): Static[] + /** + * Returns the elements of an array that meet the condition specified in a callback function. + * @param predicate A function that accepts up to three arguments. The filter method calls the predicate function one time for each element in the array. + * @param thisArg An object to which the this keyword can refer in the predicate function. If thisArg is omitted, undefined is used as the this value. + */ + public filter(predicate: (value: T, index: number, array: T[]) => unknown, thisArg?: any): T[] + public filter(predicate: any, thisArg: any): any { + return this.#values.filter(predicate as any, thisArg) + } + /** + * Calls the specified callback function for all the elements in an array. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduce method calls the callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start the accumulation. The first call to the callbackfn function provides this value as an argument instead of an array value. + */ + public reduce(callbackfn: (previousValue: Static, currentValue: Static, currentIndex: number, array: T[]) => Static): Static + public reduce(callbackfn: (previousValue: Static, currentValue: Static, currentIndex: number, array: T[]) => Static, initialValue: Static): Static + /** + * Calls the specified callback function for all the elements in an array. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduce method calls the callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start the accumulation. The first call to the callbackfn function provides this value as an argument instead of an array value. + */ + public reduce(callbackfn: (previousValue: U, currentValue: Static, currentIndex: number, array: Static[]) => U, initialValue: U): U + public reduce(callbackfn: any, initialValue?: any): any { + return this.#values.reduce(callbackfn, initialValue) + } + /** + * Calls the specified callback function for all the elements in an array, in descending order. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduceRight method calls the callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start the accumulation. The first call to the callbackfn function provides this value as an argument instead of an array value. + */ + public reduceRight(callbackfn: (previousValue: Static, currentValue: Static, currentIndex: number, array: Static[]) => Static): Static + public reduceRight(callbackfn: (previousValue: Static, currentValue: Static, currentIndex: number, array: Static[]) => Static, initialValue: T): Static + /** + * Calls the specified callback function for all the elements in an array, in descending order. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduceRight method calls the callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start the accumulation. The first call to the callbackfn function provides this value as an argument instead of an array value. + */ + public reduceRight(callbackfn: (previousValue: U, currentValue: Static, currentIndex: number, array: Static[]) => U, initialValue: U): U + public reduceRight(callbackfn: any, initialValue?: any): any { + return this.#values.reduceRight(callbackfn, initialValue) + } + // --------------------------------------------------- + // Assertions + // --------------------------------------------------- + #formatError(errors: ValueError[]) { + return errors.map((error) => `${error.message} ${error.path}`).join('. ') + } + /** Asserts the given values */ + #assertIndexInBounds(index: number) { + if (index >= 0 && index < this.#values.length) return + throw new TypeArrayError(`Index ${index} is outside the bounds of this Array.`) + } + /** Asserts the given values */ + #assertItem(index: number, item: unknown): asserts item is Static { + if (this.#typeCheck.Check(item)) return + const message = this.#formatError([...this.#typeCheck.Errors(item)]) + throw new TypeArrayError(`Item at Index ${index} is invalid. ${message}`) + } + /** Asserts the given values */ + #assertItems(items: unknown[]): asserts items is Static[] { + for (let i = 0; i < items.length; i++) { + this.#assertItem(i, items[i]) + } + } + /** Creates a typed array from an existing array */ + public static from(schema: T, iterable: IterableIterator> | Array>): TypeArray { + if (globalThis.Array.isArray(iterable)) { + const array = new TypeArray(schema, iterable.length) + for (let i = 0; i < iterable.length; i++) { + array.set(i, iterable[i]) + } + return array + } + const array = new TypeArray(schema) + for (const value of iterable) { + array.push(value) + } + return array + } +} diff --git a/example/collections/index.ts b/example/collections/index.ts new file mode 100644 index 000000000..a5187f8b5 --- /dev/null +++ b/example/collections/index.ts @@ -0,0 +1,31 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/collections + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './array' +export * from './map' +export * from './set' diff --git a/example/collections/map.ts b/example/collections/map.ts new file mode 100644 index 000000000..ac5174f88 --- /dev/null +++ b/example/collections/map.ts @@ -0,0 +1,156 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/collections + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { TypeCheck, TypeCompiler, ValueError } from '@sinclair/typebox/compiler' +import { TSchema, Static, TypeBoxError } from '@sinclair/typebox' +import { Value } from '@sinclair/typebox/value' + +// ---------------------------------------------------------------- +// TypeMapKeyError +// ---------------------------------------------------------------- +export class TypeMapKeyError extends TypeBoxError { + constructor(message: string) { + super(`${message} for key`) + } +} +export class TypeMapValueError extends TypeBoxError { + constructor(key: unknown, message: string) { + super(`${message} for key ${JSON.stringify(key)}`) + } +} +// ---------------------------------------------------------------- +// TypeMap +// ---------------------------------------------------------------- +// prettier-ignore +type TypeMapEntries = + | Iterable<[Static, Static]> + | Array<[Static, Static]> +/** Runtime type checked Map collection */ +export class TypeMap { + readonly #keycheck: TypeCheck + readonly #valuecheck: TypeCheck + readonly #keys: Map> + readonly #values: Map> + /** Constructs a new HashMap of the given key and value types. */ + constructor(key: K, value: V, entries: TypeMapEntries = []) { + this.#keycheck = TypeCompiler.Compile(key) + this.#valuecheck = TypeCompiler.Compile(value) + this.#keys = new Map>() + this.#values = new Map>() + for (const [key, value] of entries) { + this.set(key, value) + } + } + /** Iterator for this TypeMap */ + public *[Symbol.iterator](): IterableIterator<[Static, Static]> { + for (const [key, value] of this.#values) { + yield [this.#keys.get(key)!, value] + } + } + /** Iterator for the keys in this TypeMap */ + public *keys(): IterableIterator> { + yield* this.#keys.values() + } + /** Iterator for the values in this TypeMap */ + public *values(): IterableIterator> { + yield* this.#values.values() + } + /** Clears all entries in this map */ + public clear(): void { + this.#values.clear() + this.#keys.clear() + } + /** Executes a provided function once per each key/value pair in the Map, in insertion order. */ + public forEach(callbackfn: (value: Static, key: Static, map: TypeMap) => void, thisArg?: any): void { + this.#values.forEach((value, key) => callbackfn(value, this.#keys.get(key)!, this)) + } + /** @returns the number of elements in the TypeMap. */ + public get size(): number { + return this.#values.size + } + /** + * @returns boolean indicating whether an element with the specified key exists or not. + */ + public has(key: Static): boolean { + this.#assertKey(key) + return this.#values.has(this.#encodeKey(key)) + } + /** + * Returns a specified element from the Map object. If the value that is associated to the provided key is an object, then you will get a reference to that object and any change made to that object will effectively modify it inside the Map. + * @returns Returns the element associated with the specified key. If no element is associated with the specified key, undefined is returned. + */ + public get(key: Static): Static | undefined { + this.#assertKey(key) + return this.#values.get(this.#encodeKey(key))! + } + /** + * Adds a new element with a specified key and value to the Map. If an element with the same key already exists, the element will be updated. + */ + public set(key: Static, value: Static) { + this.#assertKey(key) + this.#assertValue(key, value) + const encodedKey = this.#encodeKey(key) + this.#keys.set(encodedKey, key) + this.#values.set(encodedKey, value) + } + /** + * @returns true if an element in the Map existed and has been removed, or false if the element does not exist. + */ + public delete(key: Static): boolean { + this.#assertKey(key) + const encodedKey = this.#encodeKey(key) + this.#keys.delete(encodedKey) + return this.#values.delete(encodedKey) + } + // --------------------------------------------------- + // Encoder + // --------------------------------------------------- + /** Encodes the key as a 64bit numeric */ + #encodeKey(key: Static) { + return Value.Hash(key) + } + // --------------------------------------------------- + // Assertions + // --------------------------------------------------- + #formatError(errors: ValueError[]) { + return errors + .map((error) => `${error.message} ${error.path}`) + .join('. ') + .trim() + } + /** Asserts the key matches the key schema */ + #assertKey(key: unknown): asserts key is Static { + if (this.#keycheck.Check(key)) return + throw new TypeMapKeyError(this.#formatError([...this.#keycheck.Errors(key)])) + } + /** Asserts the key matches the value schema */ + #assertValue(key: Static, value: unknown): asserts value is Static { + if (this.#valuecheck.Check(value)) return + throw new TypeMapValueError(key, this.#formatError([...this.#valuecheck.Errors(value)])) + } +} diff --git a/example/collections/readme.md b/example/collections/readme.md new file mode 100644 index 000000000..9c7adf48f --- /dev/null +++ b/example/collections/readme.md @@ -0,0 +1,3 @@ +# Collections + +This example implements runtime type safe generic `Array`, `Map` and `Set` collection types using TypeBox types as the generic type arguments. \ No newline at end of file diff --git a/example/collections/set.ts b/example/collections/set.ts new file mode 100644 index 000000000..70f44bb91 --- /dev/null +++ b/example/collections/set.ts @@ -0,0 +1,109 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/collections + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { TypeCheck, TypeCompiler, ValueError } from '@sinclair/typebox/compiler' +import { TSchema, Static, TypeBoxError } from '@sinclair/typebox' +import { Value } from '@sinclair/typebox/value' + +// ---------------------------------------------------------------- +// Errors +// ---------------------------------------------------------------- +export class TypeSetError extends TypeBoxError { + constructor(message: string) { + super(`${message}`) + } +} +// ---------------------------------------------------------------- +// TypeSet +// ---------------------------------------------------------------- +/** Runtime type checked Set collection */ +export class TypeSet { + readonly #valuecheck: TypeCheck + readonly values: Map> + constructor(schema: T, iterable: Array | Iterable = []) { + this.#valuecheck = TypeCompiler.Compile(schema) + this.values = new Map>() + for (const value of iterable) { + this.add(value) + } + } + /** Adds a value to this set */ + public add(value: Static): this { + this.#assertValue(value) + this.values.set(this.#encodeKey(value), value) + return this + } + /** Clears the values in this set */ + public clear(): void { + this.values.clear() + } + /** + * Removes a specified value from the Set. + * @returns Returns true if an element in the Set existed and has been removed, or false if the element does not exist. + */ + public delete(value: Static): boolean { + return this.values.delete(this.#encodeKey(value)) + } + /** Executes a provided function once per each value in the Set object, in insertion order. */ + public forEach(callbackfn: (value: Static, value2: Static, set: TypeSet) => void, thisArg?: any): void { + this.values.forEach((value, value2) => callbackfn(value, value2, this), thisArg) + } + /** + * @returns a boolean indicating whether an element with the specified value exists in the Set or not. + */ + public has(value: Static): boolean { + return this.values.has(this.#encodeKey(value)) + } + /** + * @returns the number of (unique) elements in Set. + */ + public get size(): number { + return this.values.size + } + // --------------------------------------------------- + // Encoder + // --------------------------------------------------- + #encodeKey(value: Static) { + return Value.Hash(value) + } + // --------------------------------------------------- + // Assertions + // --------------------------------------------------- + /** Formats errors */ + #formatError(errors: ValueError[]) { + return errors + .map((error) => `${error.message} ${error.path}`) + .join('. ') + .trim() + } + /** Asserts the key matches the value schema */ + #assertValue(value: unknown): asserts value is Static { + if (this.#valuecheck.Check(value)) return + throw new TypeSetError(this.#formatError([...this.#valuecheck.Errors(value)])) + } +} diff --git a/example/formats/date-time.ts b/example/formats/date-time.ts new file mode 100644 index 000000000..ed8ac29ac --- /dev/null +++ b/example/formats/date-time.ts @@ -0,0 +1,39 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/format + +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { IsDate } from './date' +import { IsTime } from './time' + +const DATE_TIME_SEPARATOR = /t|\s/i + +/** + * `[ajv-formats]` ISO8601 DateTime + * @example `2020-12-12T20:20:40+00:00` + */ +export function IsDateTime(value: string, strictTimeZone?: boolean): boolean { + const dateTime: string[] = value.split(DATE_TIME_SEPARATOR) + return dateTime.length === 2 && IsDate(dateTime[0]) && IsTime(dateTime[1], strictTimeZone) +} diff --git a/example/formats/date.ts b/example/formats/date.ts new file mode 100644 index 000000000..fc006341b --- /dev/null +++ b/example/formats/date.ts @@ -0,0 +1,44 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/format + +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +const DAYS = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] +const DATE = /^(\d\d\d\d)-(\d\d)-(\d\d)$/ + +function IsLeapYear(year: number): boolean { + return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) +} +/** + * `[ajv-formats]` ISO8601 Date component + * @example `2020-12-12` + */ +export function IsDate(value: string): boolean { + const matches: string[] | null = DATE.exec(value) + if (!matches) return false + const year: number = +matches[1] + const month: number = +matches[2] + const day: number = +matches[3] + return month >= 1 && month <= 12 && day >= 1 && day <= (month === 2 && IsLeapYear(year) ? 29 : DAYS[month]) +} diff --git a/example/formats/email.ts b/example/formats/email.ts new file mode 100644 index 000000000..430b7e470 --- /dev/null +++ b/example/formats/email.ts @@ -0,0 +1,35 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/format + +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +const Email = /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i + +/** + * `[ajv-formats]` Internet Email Address [RFC 5321, section 4.1.2.](http://tools.ietf.org/html/rfc5321#section-4.1.2) + * @example `user@domain.com` + */ +export function IsEmail(value: string): boolean { + return Email.test(value) +} diff --git a/example/formats/index.ts b/example/formats/index.ts new file mode 100644 index 000000000..cd002284d --- /dev/null +++ b/example/formats/index.ts @@ -0,0 +1,36 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/format + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './date-time' +export * from './date' +export * from './email' +export * from './ipv4' +export * from './ipv6' +export * from './time' +export * from './url' +export * from './uuid' diff --git a/example/formats/ipv4.ts b/example/formats/ipv4.ts new file mode 100644 index 000000000..1cf506988 --- /dev/null +++ b/example/formats/ipv4.ts @@ -0,0 +1,35 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/format + +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +const IPv4 = /^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/ + +/** + * `[ajv-formats]` IPv4 address according to dotted-quad ABNF syntax as defined in [RFC 2673, section 3.2](http://tools.ietf.org/html/rfc2673#section-3.2) + * @example `192.168.0.1` + */ +export function IsIPv4(value: string): boolean { + return IPv4.test(value) +} diff --git a/example/formats/ipv6.ts b/example/formats/ipv6.ts new file mode 100644 index 000000000..819be3ff2 --- /dev/null +++ b/example/formats/ipv6.ts @@ -0,0 +1,36 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/format + +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +const IPv6 = + /^((([0-9a-f]{1,4}:){7}([0-9a-f]{1,4}|:))|(([0-9a-f]{1,4}:){6}(:[0-9a-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){5}(((:[0-9a-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){4}(((:[0-9a-f]{1,4}){1,3})|((:[0-9a-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){3}(((:[0-9a-f]{1,4}){1,4})|((:[0-9a-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){2}(((:[0-9a-f]{1,4}){1,5})|((:[0-9a-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){1}(((:[0-9a-f]{1,4}){1,6})|((:[0-9a-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9a-f]{1,4}){1,7})|((:[0-9a-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))$/i + +/** + * `[ajv-formats]` IPv6 address as defined in [RFC 2373, section 2.2](http://tools.ietf.org/html/rfc2373#section-2.2). + * @example `2001:0db8:85a3:0000:0000:8a2e:0370:7334` + */ +export function IsIPv6(value: string): boolean { + return IPv6.test(value) +} diff --git a/example/formats/time.ts b/example/formats/time.ts new file mode 100644 index 000000000..9002f6908 --- /dev/null +++ b/example/formats/time.ts @@ -0,0 +1,48 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/format + +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +const TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i + +/** + * `[ajv-formats]` ISO8601 Time component + * @example `20:20:39+00:00` + */ +export function IsTime(value: string, strictTimeZone?: boolean): boolean { + const matches: string[] | null = TIME.exec(value) + if (!matches) return false + const hr: number = +matches[1] + const min: number = +matches[2] + const sec: number = +matches[3] + const tz: string | undefined = matches[4] + const tzSign: number = matches[5] === '-' ? -1 : 1 + const tzH: number = +(matches[6] || 0) + const tzM: number = +(matches[7] || 0) + if (tzH > 23 || tzM > 59 || (strictTimeZone && !tz)) return false + if (hr <= 23 && min <= 59 && sec < 60) return true + const utcMin = min - tzM * tzSign + const utcHr = hr - tzH * tzSign - (utcMin < 0 ? 1 : 0) + return (utcHr === 23 || utcHr === -1) && (utcMin === 59 || utcMin === -1) && sec < 61 +} diff --git a/example/formats/url.ts b/example/formats/url.ts new file mode 100644 index 000000000..c01091eda --- /dev/null +++ b/example/formats/url.ts @@ -0,0 +1,36 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/format + +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +const Url = + /^(?:https?|wss?|ftp):\/\/(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u{00a1}-\u{ffff}]+-)*[a-z0-9\u{00a1}-\u{ffff}]+)(?:\.(?:[a-z0-9\u{00a1}-\u{ffff}]+-)*[a-z0-9\u{00a1}-\u{ffff}]+)*(?:\.(?:[a-z\u{00a1}-\u{ffff}]{2,})))(?::\d{2,5})?(?:\/[^\s]*)?$/iu + +/** + * `[ajv-formats:deprecated]` A uniform resource locator as defined in [RFC 1738](https://www.rfc-editor.org/rfc/rfc1738) + * @example `http://domain.com` + */ +export function IsUrl(value: string) { + return Url.test(value) +} diff --git a/example/formats/uuid.ts b/example/formats/uuid.ts new file mode 100644 index 000000000..6213fba69 --- /dev/null +++ b/example/formats/uuid.ts @@ -0,0 +1,35 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/format + +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +const Uuid = /^(?:urn:uuid:)?[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i + +/** + * `[ajv-formats]` A Universally Unique Identifier as defined by [RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122). + * @example `9aa8a673-8590-4db2-9830-01755844f7c1` + */ +export function IsUuid(value: string): boolean { + return Uuid.test(value) +} diff --git a/example/index.ts b/example/index.ts index 922c43620..58b1d1370 100644 --- a/example/index.ts +++ b/example/index.ts @@ -1,15 +1,49 @@ -import { Type, Static } from '@sinclair/typebox' +import { TypeSystem } from '@sinclair/typebox/system' +import { TypeCompiler } from '@sinclair/typebox/compiler' +import { Value, ValuePointer } from '@sinclair/typebox/value' +import { Type, TypeGuard, Kind, Static, TSchema } from '@sinclair/typebox' +import { Syntax } from '@sinclair/typebox/syntax' + +// ----------------------------------------------------------- +// Create: Type +// ----------------------------------------------------------- const T = Type.Object({ - name: Type.Optional(Type.String()), - order: Type.Number() -}, { $id: 'T' }) + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), +}) + +type T = Static + +console.log(T) + +// ----------------------------------------------------------- +// Syntax: Type +// ----------------------------------------------------------- + +const S = Syntax({ T }, `{ x: T, y: T, z: T }`) + +type S = Static + +// ----------------------------------------------------------- +// Create: Value +// ----------------------------------------------------------- + +const V = Value.Create(T) + +console.log(V) -const R = Type.Ref(T) +// ----------------------------------------------------------- +// Compile: Type +// ----------------------------------------------------------- -const P = Type.Omit(T, ['name']) +const C = TypeCompiler.Compile(T) -console.log(P) +console.log(C.Code()) -type T = Static +// ----------------------------------------------------------- +// Check: Value +// ----------------------------------------------------------- +console.log(C.Check(V)) diff --git a/example/prototypes/discriminated-union.ts b/example/prototypes/discriminated-union.ts new file mode 100644 index 000000000..8aae1f3f2 --- /dev/null +++ b/example/prototypes/discriminated-union.ts @@ -0,0 +1,96 @@ + +/*-------------------------------------------------------------------------- + +@sinclair/typebox/prototypes + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { Static, Kind, TSchema, TObject, SchemaOptions, CreateType, TLiteral, TypeRegistry, ValueGuard, KindGuard, TUnion } from '@sinclair/typebox' +import { GetErrorFunction, SetErrorFunction } from 'src/errors/function' +import { Value } from '@sinclair/typebox/value' + +// ------------------------------------------------------------------ +// DiscriminatedUnionError +// ------------------------------------------------------------------ +const errorFunction = GetErrorFunction() + +// prettier-ignore +SetErrorFunction((parameter) => { + if (parameter.schema[Kind] !== 'DiscriminatedUnion') { + return errorFunction(parameter) + } + const union = parameter.schema as TDiscriminatedUnion + // Try generate error when value matches known discriminator literal + if (ValueGuard.IsObject(parameter.value) && union.discriminator in parameter.value) { + const variant = parameter.schema.anyOf.find((variant: TSchema) => union.discriminator in variant.properties + && (variant.properties[union.discriminator] as TLiteral).const === + (parameter.value as Record)[union.discriminator]) + if (KindGuard.IsSchema(variant)) { + const literal = variant.properties[union.discriminator] + return `Invalid value for DiscriminatedUnion variant '${literal.const}'` + } + } + // Return generic error containing possible discriminator types. + const options = union.anyOf.map(object => object.properties[union.discriminator].const) as string[] + return `Expected value of ${options.map(option => `'${option}'`).join(', ')} for DiscriminatedUnion` +}) + +// ------------------------------------------------------------------ +// TDiscriminatedUnionObject +// +// Constructs a base TObject type requiring 1 discriminator property +// ------------------------------------------------------------------ +// prettier-ignore +type TDiscriminatedUnionProperties = { + [_ in Discriminator]: TLiteral +} +// prettier-ignore +type TDiscriminatedUnionObject = TObject> + +// ------------------------------------------------------------------ +// DiscriminatedUnion +// ------------------------------------------------------------------ +// prettier-ignore +TypeRegistry.Set('DiscriminatedUnion', (schema: TDiscriminatedUnion, value) => { + return schema.anyOf.some(variant => Value.Check(variant, [], value)) +}) +// prettier-ignore +export interface TDiscriminatedUnion extends TSchema { + [Kind]: 'DiscriminatedUnion' + static: Static> + discriminator: Discriminator + anyOf: Types +} + +/** Creates a DiscriminatedUnion. */ +// prettier-ignore +export function DiscriminatedUnion[]>( + discriminator: Discriminator, types: [...Types], options?: SchemaOptions +): TDiscriminatedUnion { + return CreateType({ [Kind]: 'DiscriminatedUnion', anyOf: types, discriminator }, options) as never +} + + + diff --git a/example/prototypes/from-schema.ts b/example/prototypes/from-schema.ts new file mode 100644 index 000000000..31cc374da --- /dev/null +++ b/example/prototypes/from-schema.ts @@ -0,0 +1,251 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/prototypes + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import * as Type from '@sinclair/typebox' + +// ------------------------------------------------------------------ +// Schematics +// ------------------------------------------------------------------ +const IsExact = (value: unknown, expect: unknown) => value === expect +const IsSValue = (value: unknown): value is SValue => Type.ValueGuard.IsString(value) || Type.ValueGuard.IsNumber(value) || Type.ValueGuard.IsBoolean(value) +const IsSEnum = (value: unknown): value is SEnum => Type.ValueGuard.IsObject(value) && Type.ValueGuard.IsArray(value.enum) && value.enum.every((value) => IsSValue(value)) +const IsSAllOf = (value: unknown): value is SAllOf => Type.ValueGuard.IsObject(value) && Type.ValueGuard.IsArray(value.allOf) +const IsSAnyOf = (value: unknown): value is SAnyOf => Type.ValueGuard.IsObject(value) && Type.ValueGuard.IsArray(value.anyOf) +const IsSOneOf = (value: unknown): value is SOneOf => Type.ValueGuard.IsObject(value) && Type.ValueGuard.IsArray(value.oneOf) +const IsSTuple = (value: unknown): value is STuple => Type.ValueGuard.IsObject(value) && IsExact(value.type, 'array') && Type.ValueGuard.IsArray(value.items) +const IsSArray = (value: unknown): value is SArray => Type.ValueGuard.IsObject(value) && IsExact(value.type, 'array') && !Type.ValueGuard.IsArray(value.items) && Type.ValueGuard.IsObject(value.items) +const IsSConst = (value: unknown): value is SConst => Type.ValueGuard.IsObject(value) && Type.ValueGuard.IsObject(value['const']) +const IsSString = (value: unknown): value is SString => Type.ValueGuard.IsObject(value) && IsExact(value.type, 'string') +const IsSRef = (value: unknown): value is SRef => Type.ValueGuard.IsObject(value) && Type.ValueGuard.IsString(value.$ref) +const IsSNumber = (value: unknown): value is SNumber => Type.ValueGuard.IsObject(value) && IsExact(value.type, 'number') +const IsSInteger = (value: unknown): value is SInteger => Type.ValueGuard.IsObject(value) && IsExact(value.type, 'integer') +const IsSBoolean = (value: unknown): value is SBoolean => Type.ValueGuard.IsObject(value) && IsExact(value.type, 'boolean') +const IsSNull = (value: unknown): value is SBoolean => Type.ValueGuard.IsObject(value) && IsExact(value.type, 'null') +const IsSProperties = (value: unknown): value is SProperties => Type.ValueGuard.IsObject(value) +// prettier-ignore +const IsSObject = (value: unknown): value is SObject => Type.ValueGuard.IsObject(value) && IsExact(value.type, 'object') && IsSProperties(value.properties) && (value.required === undefined || Type.ValueGuard.IsArray(value.required) && value.required.every((value: unknown) => Type.ValueGuard.IsString(value))) +type SValue = string | number | boolean +type SEnum = Readonly<{ enum: readonly SValue[] }> +type SAllOf = Readonly<{ allOf: readonly unknown[] }> +type SAnyOf = Readonly<{ anyOf: readonly unknown[] }> +type SOneOf = Readonly<{ oneOf: readonly unknown[] }> +type SProperties = Record +type SObject = Readonly<{ type: 'object'; properties: SProperties; required?: readonly string[] }> +type STuple = Readonly<{ type: 'array'; items: readonly unknown[] }> +type SArray = Readonly<{ type: 'array'; items: unknown }> +type SConst = Readonly<{ const: SValue }> +type SRef = Readonly<{ $ref: string }> +type SString = Readonly<{ type: 'string' }> +type SNumber = Readonly<{ type: 'number' }> +type SInteger = Readonly<{ type: 'integer' }> +type SBoolean = Readonly<{ type: 'boolean' }> +type SNull = Readonly<{ type: 'null' }> +// ------------------------------------------------------------------ +// FromRest +// ------------------------------------------------------------------ +// prettier-ignore +type TFromRest = ( + T extends readonly [infer L extends unknown, ...infer R extends unknown[]] + ? TFromSchema extends infer S extends Type.TSchema + ? TFromRest + : TFromRest + : Acc +) +function FromRest(T: T): TFromRest { + return T.map((L) => FromSchema(L)) as never +} +// ------------------------------------------------------------------ +// FromEnumRest +// ------------------------------------------------------------------ +// prettier-ignore +type TFromEnumRest = ( + T extends readonly [infer L extends SValue, ...infer R extends SValue[]] + ? TFromEnumRest]> + : Acc +) +function FromEnumRest(T: T): TFromEnumRest { + return T.map((L) => Type.Literal(L)) as never +} +// ------------------------------------------------------------------ +// AllOf +// ------------------------------------------------------------------ +// prettier-ignore +type TFromAllOf = ( + TFromRest extends infer Rest extends Type.TSchema[] + ? Type.TIntersectEvaluated + : Type.TNever +) +function FromAllOf(T: T): TFromAllOf { + return Type.IntersectEvaluated(FromRest(T.allOf), T) +} +// ------------------------------------------------------------------ +// AnyOf +// ------------------------------------------------------------------ +// prettier-ignore +type TFromAnyOf = ( + TFromRest extends infer Rest extends Type.TSchema[] + ? Type.TUnionEvaluated + : Type.TNever +) +function FromAnyOf(T: T): TFromAnyOf { + return Type.UnionEvaluated(FromRest(T.anyOf), T) +} +// ------------------------------------------------------------------ +// OneOf +// ------------------------------------------------------------------ +// prettier-ignore +type TFromOneOf = ( + TFromRest extends infer Rest extends Type.TSchema[] + ? Type.TUnionEvaluated + : Type.TNever +) +function FromOneOf(T: T): TFromOneOf { + return Type.UnionEvaluated(FromRest(T.oneOf), T) +} +// ------------------------------------------------------------------ +// Enum +// ------------------------------------------------------------------ +// prettier-ignore +type TFromEnum = ( + TFromEnumRest extends infer Elements extends Type.TSchema[] + ? Type.TUnionEvaluated + : Type.TNever +) +function FromEnum(T: T): TFromEnum { + return Type.UnionEvaluated(FromEnumRest(T.enum)) +} +// ------------------------------------------------------------------ +// Tuple +// ------------------------------------------------------------------ +// prettier-ignore +type TFromTuple = ( + TFromRest extends infer Elements extends Type.TSchema[] + ? Type.TTuple + : Type.TTuple<[]> +) +// prettier-ignore +function FromTuple(T: T): TFromTuple { + return Type.Tuple(FromRest(T.items), T) as never +} +// ------------------------------------------------------------------ +// Array +// ------------------------------------------------------------------ +// prettier-ignore +type TFromArray = ( + TFromSchema extends infer Items extends Type.TSchema + ? Type.TArray + : Type.TArray +) +// prettier-ignore +function FromArray(T: T): TFromArray { + return Type.Array(FromSchema(T.items), T) as never +} +// ------------------------------------------------------------------ +// Const +// ------------------------------------------------------------------ +// prettier-ignore +type TFromConst = ( + Type.Ensure> +) +function FromConst(T: T) { + return Type.Literal(T.const, T) +} +// ------------------------------------------------------------------ +// Ref +// ------------------------------------------------------------------ +// prettier-ignore +type TFromRef = ( + Type.Ensure> +) +function FromRef(T: T) { + return Type.Ref(T['$ref']) +} +// ------------------------------------------------------------------ +// Object +// ------------------------------------------------------------------ +type TFromPropertiesIsOptional = unknown extends R ? true : K extends R ? false : true +// prettier-ignore +type TFromProperties = Type.Evaluate<{ + -readonly [K in keyof T]: TFromPropertiesIsOptional extends true + ? Type.TOptional> + : TFromSchema +}> +// prettier-ignore +type TFromObject = ( + TFromProperties[number]> extends infer Properties extends Type.TProperties + ? Type.TObject + : Type.TObject<{}> +) +function FromObject(T: T): TFromObject { + const properties = globalThis.Object.getOwnPropertyNames(T.properties).reduce((Acc, K) => { + return { ...Acc, [K]: T.required && T.required.includes(K) ? FromSchema(T.properties[K]) : Type.Optional(FromSchema(T.properties[K])) } + }, {} as Type.TProperties) + return Type.Object(properties, T) as never +} +// ------------------------------------------------------------------ +// FromSchema +// ------------------------------------------------------------------ +// prettier-ignore +export type TFromSchema = ( + T extends SAllOf ? TFromAllOf : + T extends SAnyOf ? TFromAnyOf : + T extends SOneOf ? TFromOneOf : + T extends SEnum ? TFromEnum : + T extends SObject ? TFromObject : + T extends STuple ? TFromTuple : + T extends SArray ? TFromArray : + T extends SConst ? TFromConst : + T extends SRef ? TFromRef : + T extends SString ? Type.TString : + T extends SNumber ? Type.TNumber : + T extends SInteger ? Type.TInteger : + T extends SBoolean ? Type.TBoolean : + T extends SNull ? Type.TNull : + Type.TUnknown +) +/** Parses a TypeBox type from raw JsonSchema */ +export function FromSchema(T: T): TFromSchema { + // prettier-ignore + return ( + IsSAllOf(T) ? FromAllOf(T) : + IsSAnyOf(T) ? FromAnyOf(T) : + IsSOneOf(T) ? FromOneOf(T) : + IsSEnum(T) ? FromEnum(T) : + IsSObject(T) ? FromObject(T) : + IsSTuple(T) ? FromTuple(T) : + IsSArray(T) ? FromArray(T) : + IsSConst(T) ? FromConst(T) : + IsSRef(T) ? FromRef(T) : + IsSString(T) ? Type.String(T) : + IsSNumber(T) ? Type.Number(T) : + IsSInteger(T) ? Type.Integer(T) : + IsSBoolean(T) ? Type.Boolean(T) : + IsSNull(T) ? Type.Null(T) : + Type.Unknown(T || {}) + ) as never +} diff --git a/example/prototypes/index.ts b/example/prototypes/index.ts new file mode 100644 index 000000000..979519c61 --- /dev/null +++ b/example/prototypes/index.ts @@ -0,0 +1,35 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/prototypes + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './discriminated-union' +export * from './from-schema' +export * from './options' +export * from './partial-deep' +export * from './recursive-map' +export * from './union-enum' +export * from './union-oneof' diff --git a/example/prototypes/options.ts b/example/prototypes/options.ts new file mode 100644 index 000000000..bf09281ad --- /dev/null +++ b/example/prototypes/options.ts @@ -0,0 +1,40 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/prototypes + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { TSchema, CloneType } from '@sinclair/typebox' + +// prettier-ignore +export type TOptions> = ( + Type & Options +) + +/** `[Prototype]` Augments a schema with additional generics aware properties */ +// prettier-ignore +export function Options>(type: Type, options: Options): TOptions { + return CloneType(type, options) as never +} diff --git a/example/prototypes/partial-deep.ts b/example/prototypes/partial-deep.ts new file mode 100644 index 000000000..8637b52dc --- /dev/null +++ b/example/prototypes/partial-deep.ts @@ -0,0 +1,68 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/prototypes + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { TypeGuard, Type, TSchema, TIntersect, TUnion, TObject, TPartial, TProperties, Evaluate } from '@sinclair/typebox' + +// ------------------------------------------------------------------------------------- +// TPartialDeepProperties +// ------------------------------------------------------------------------------------- +export type TPartialDeepProperties = { + [K in keyof T]: TPartialDeep +} +function PartialDeepProperties(properties: T): TPartialDeepProperties { + return Object.getOwnPropertyNames(properties).reduce((acc, key) => { + return {...acc, [key]: PartialDeep(properties[key])} + }, {}) as never +} +// ------------------------------------------------------------------------------------- +// TPartialDeepRest +// ------------------------------------------------------------------------------------- +export type TPartialDeepRest = ( + T extends [infer L extends TSchema, ...infer R extends TSchema[]] + ? TPartialDeepRest]> + : Acc +) +function PartialDeepRest(rest: [...T]): TPartialDeepRest { + return rest.map(schema => PartialDeep(schema)) as never +} +// ------------------------------------------------------------------------------------- +// TPartialDeep +// ------------------------------------------------------------------------------------- +export type TPartialDeep = + T extends TIntersect ? TIntersect> : + T extends TUnion ? TUnion> : + T extends TObject ? TPartial>>> : + T +export function PartialDeep(schema: T): TPartialDeep { + return ( + TypeGuard.IsIntersect(schema) ? Type.Intersect(PartialDeepRest(schema.allOf)) : + TypeGuard.IsUnion(schema) ? Type.Union(PartialDeepRest(schema.anyOf)) : + TypeGuard.IsObject(schema) ? Type.Partial(Type.Object(PartialDeepProperties(schema.properties))) : + schema + ) as never +} \ No newline at end of file diff --git a/example/prototypes/readme.md b/example/prototypes/readme.md new file mode 100644 index 000000000..bf5ea9060 --- /dev/null +++ b/example/prototypes/readme.md @@ -0,0 +1,119 @@ +# TypeBox Prototypes + +TypeBox prototypes are a set of types that are either under consideration for inclusion into the library, or have been requested by users but cannot be added to the library either due to complexity, using schematics that fall outside the supported TypeBox or should be expressed by users via advanced type composition. + + +## PartialDeep + +Maps the given schema as deep partial, making all properties and sub properties optional. This type is asked for on occation, but as there is no TypeScript equivalent and because this type is typically handled through end user type mapping, this type is left out of TypeBox. + +```typescript +import { PartialDeep } from './prototypes' + +const T = Type.Object({ + x: Type.Object({ + x: Type.Number(), + y: Type.Number() + }), + y: Type.Object({ + x: Type.Number(), + y: Type.Number() + }) +}) + +const P = PartialDeep(T) + +type P = Static // type P = { + // x?: { + // x?: number, + // y?: number + // }, + // y?: { + // x?: number, + // y?: number + // }, + // } +``` + +## UnionEnum + +Creates an `enum` union string schema representation. This type is often requested by OpenAPI users, particularily for documentation presentation. As TypeBox standardizes on `anyOf` for all unions, this type is generally at odds with TypeBox's internal representation. Some considerations for internally remapping this type into a `anyOf` through composition have been considered (and would be feasible), but as TypeScript doesn't have multiple representations for unions, neither should TypeBox, making this type an unlikely candidate. + +```typescript +import { UnionEnum } from './prototypes' + + +const T = UnionEnum(['A', 'B', 'C']) // const T = { + // enum: ['A', 'B', 'C'] + // } + +type T = Static // type T = 'A' | 'B' | 'C' + +``` +## UnionOneOf + +Creates a `oneOf` union representation. This type is often requested by users looking for discriminated union support (which is not formally supported by JSON Schema). TypeBox omits this type as `oneOf` has the potential to create illogical schematics where values match more than one sub schema (making type inference extremely difficult). TypeBox preferences users explicitly narrowing on a overlapping union post type check, making `anyOf` the ideal representation, leaving the `oneOf` type an unlikely candidate for inclusion in the library. + + +```typescript +import { UnionOneOf } from './prototypes' + + +const T = UnionOneOf([ // const T = { + Type.Literal('A'), // oneOf: [ + Type.Literal('B'), // { const: 'A' }, + Type.Literal('C') // { const: 'B' }, +]) // { const: 'C' }, + // ] + // } + +type T = Static // type T = 'A' | 'B' | 'C' + +``` + +## Options + +By default, TypeBox does not represent arbituary options as generics aware properties. However, there are cases where having options observable to the type system can be useful, for example conditionally mapping schematics based on custom metadata. The Options function makes user defined options generics aware. + +```typescript +import { Options } from './prototypes' + +const A = Options(Type.String(), { foo: 1 }) // Options + +type A = typeof A extends { foo: number } ? true : false // true: foo property is observable to the type system +``` + +## Recursive Map +The Recursive Map type enables deep structural remapping of a type and it's internal constituents. This type accepts a TSchema type and a mapping type function (expressed via HKT). The HKT is applied when traversing the type and it's interior. The mapping HKT can apply conditional tests to each visited type to remap into a new form. The following augments a schematic via Options, and conditionally remaps any schema with an default annotation to make it optional. +```typescript +import { Type, TOptional, Static, TSchema } from '@sinclair/typebox' + +import { TRecursiveMap, TMappingType, Options } from './prototypes' + +// ------------------------------------------------------------------ +// StaticDefault +// ------------------------------------------------------------------ +export interface StaticDefaultMapping extends TMappingType { + output: ( + this['input'] extends TSchema // if input schematic contains an default + ? this['input'] extends { default: unknown } // annotation, remap it to be optional, + ? TOptional // otherwise just return the schema as is. + : this['input'] + : this['input'] + ) +} +export type StaticDefault = ( + Static> +) + +// ------------------------------------------------------------------ +// Usage +// ------------------------------------------------------------------ + +const T = Type.Object({ + x: Options(Type.String(), { default: 'hello' }), + y: Type.String() +}) + +type T = StaticDefault // { x?: string, y: string } +type S = Static // { x: string, y: string } \ No newline at end of file diff --git a/example/prototypes/recursive-map.ts b/example/prototypes/recursive-map.ts new file mode 100644 index 000000000..4ff008309 --- /dev/null +++ b/example/prototypes/recursive-map.ts @@ -0,0 +1,153 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/prototypes + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import * as Types from '@sinclair/typebox' + +// ------------------------------------------------------------------ +// Mapping: Functions and Type +// ------------------------------------------------------------------ +export type TMappingFunction = (schema: Types.TSchema) => Types.TSchema + +export interface TMappingType { + input: unknown + output: unknown +} +// ------------------------------------------------------------------ +// Record Parameters +// ------------------------------------------------------------------ +function GetRecordPattern(record: Types.TRecord): string { + return globalThis.Object.getOwnPropertyNames(record.patternProperties)[0] +} +function GetRecordKey(record: Types.TRecord): Types.TSchema { + const pattern = GetRecordPattern(record) + return ( + pattern === Types.PatternStringExact ? Types.String() : + pattern === Types.PatternNumberExact ? Types.Number() : + pattern === Types.PatternBooleanExact ? Types.Boolean() : + Types.String({ pattern }) + ) +} +function GetRecordValue(record: Types.TRecord): Types.TSchema { + return record.patternProperties[GetRecordPattern(record)] +} +// ------------------------------------------------------------------ +// Traversal +// ------------------------------------------------------------------ +// prettier-ignore +type TApply = Result +// prettier-ignore +type TFromProperties +}> = Result +function FromProperties(properties: Types.TProperties, func: TMappingFunction): Types.TProperties { + return globalThis.Object.getOwnPropertyNames(properties).reduce((result, key) => { + return {...result, [key]: RecursiveMap(properties[key], func) } + }, {}) +} +// prettier-ignore +type TFromRest = ( + Types extends [infer Left extends Types.TSchema, ...infer Right extends Types.TSchema[]] + ? TFromRest]> + : Result +) +function FromRest(types: Types.TSchema[], func: TMappingFunction): Types.TSchema[] { + return types.map(type => RecursiveMap(type, func)) +} +// prettier-ignore +type TFromType +)> = Result +function FromType(type: Types.TSchema, func: TMappingFunction): Types.TSchema { + return func(type) +} +// ------------------------------------------------------------------ +// TRecursiveMap +// ------------------------------------------------------------------ +/** `[Prototype]` Applies a deep recursive map across the given type and sub types. */ +// prettier-ignore +export type TRecursiveMap, + // Maps the Interior Parameterized Types + Interior extends Types.TSchema = ( + Exterior extends Types.TConstructor ? Types.TConstructor, TFromType> : + Exterior extends Types.TFunction ? Types.TFunction, TFromType> : + Exterior extends Types.TIntersect ? Types.TIntersect> : + Exterior extends Types.TUnion ? Types.TUnion> : + Exterior extends Types.TTuple ? Types.TTuple> : + Exterior extends Types.TArray ? Types.TArray>: + Exterior extends Types.TAsyncIterator ? Types.TAsyncIterator> : + Exterior extends Types.TIterator ? Types.TIterator> : + Exterior extends Types.TPromise ? Types.TPromise> : + Exterior extends Types.TObject ? Types.TObject> : + Exterior extends Types.TRecord ? Types.TRecordOrObject, TFromType> : + Exterior + ), + // Modifiers Derived from Exterior Type Mapping + IsOptional extends number = Exterior extends Types.TOptional ? 1 : 0, + IsReadonly extends number = Exterior extends Types.TReadonly ? 1 : 0, + Result extends Types.TSchema = ( + [IsReadonly, IsOptional] extends [1, 1] ? Types.TReadonlyOptional : + [IsReadonly, IsOptional] extends [0, 1] ? Types.TOptional : + [IsReadonly, IsOptional] extends [1, 0] ? Types.TReadonly : + Interior + ) +> = Result +/** `[Prototype]` Applies a deep recursive map across the given type and sub types. */ +// prettier-ignore +export function RecursiveMap(type: Types.TSchema, func: TMappingFunction): Types.TSchema { + // Maps the Exterior Type + const exterior = Types.CloneType(FromType(type, func), type) + // Maps the Interior Parameterized Types + const interior = ( + Types.KindGuard.IsConstructor(type) ? Types.Constructor(FromRest(type.parameters, func), FromType(type.returns, func), exterior) : + Types.KindGuard.IsFunction(type) ? Types.Function(FromRest(type.parameters, func), FromType(type.returns, func), exterior) : + Types.KindGuard.IsIntersect(type) ? Types.Intersect(FromRest(type.allOf, func), exterior) : + Types.KindGuard.IsUnion(type) ? Types.Union(FromRest(type.anyOf, func), exterior) : + Types.KindGuard.IsTuple(type) ? Types.Tuple(FromRest(type.items || [], func), exterior) : + Types.KindGuard.IsArray(type) ? Types.Array(FromType(type.items, func), exterior) : + Types.KindGuard.IsAsyncIterator(type) ? Types.AsyncIterator(FromType(type.items, func), exterior) : + Types.KindGuard.IsIterator(type) ? Types.Iterator(FromType(type.items, func), exterior) : + Types.KindGuard.IsPromise(type) ? Types.Promise(FromType(type.items, func), exterior) : + Types.KindGuard.IsObject(type) ? Types.Object(FromProperties(type.properties, func), exterior) : + Types.KindGuard.IsRecord(type) ? Types.Record(FromType(GetRecordKey(type), func), FromType(GetRecordValue(type), func), exterior) : + Types.CloneType(exterior, exterior) + ) + // Modifiers Derived from Exterior Type Mapping + const isOptional = Types.KindGuard.IsOptional(exterior) + const isReadonly = Types.KindGuard.IsOptional(exterior) + return ( + isOptional && isReadonly ? Types.ReadonlyOptional(interior) : + isOptional ? Types.Optional(interior) : + isReadonly ? Types.Readonly(interior) : + interior + ) +} \ No newline at end of file diff --git a/example/prototypes/union-enum.ts b/example/prototypes/union-enum.ts new file mode 100644 index 000000000..ee15f15a4 --- /dev/null +++ b/example/prototypes/union-enum.ts @@ -0,0 +1,49 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/prototypes + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { TypeRegistry, Kind, TSchema, SchemaOptions } from '@sinclair/typebox' + +// ------------------------------------------------------------------------------------- +// TUnionEnum +// ------------------------------------------------------------------------------------- +export interface TUnionEnum extends TSchema { + [Kind]: 'UnionEnum' + static: T[number] + enum: T +} +// ------------------------------------------------------------------------------------- +// UnionEnum +// ------------------------------------------------------------------------------------- +/** `[Experimental]` Creates a Union type with a `enum` schema representation */ +export function UnionEnum(values: [...T], options: SchemaOptions = {}) { + function UnionEnumCheck(schema: TUnionEnum<(string | number)[]>, value: unknown) { + return (typeof value === 'string' || typeof value === 'number') && schema.enum.includes(value) + } + if (!TypeRegistry.Has('UnionEnum')) TypeRegistry.Set('UnionEnum', UnionEnumCheck) + return { ...options, [Kind]: 'UnionEnum', enum: values } as TUnionEnum +} \ No newline at end of file diff --git a/example/prototypes/union-oneof.ts b/example/prototypes/union-oneof.ts new file mode 100644 index 000000000..ec14fac94 --- /dev/null +++ b/example/prototypes/union-oneof.ts @@ -0,0 +1,50 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/prototypes + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { TypeRegistry, Kind, Static, TSchema, SchemaOptions } from '@sinclair/typebox' +import { Value } from '@sinclair/typebox/value' + +// ------------------------------------------------------------------------------------- +// TUnionOneOf +// ------------------------------------------------------------------------------------- +export interface TUnionOneOf extends TSchema { + [Kind]: 'UnionOneOf' + static: { [K in keyof T]: Static }[number] + oneOf: T +} +// ------------------------------------------------------------------------------------- +// UnionOneOf +// ------------------------------------------------------------------------------------- +/** `[Experimental]` Creates a Union type with a `oneOf` schema representation */ +export function UnionOneOf(oneOf: [...T], options: SchemaOptions = {}) { + function UnionOneOfCheck(schema: TUnionOneOf, value: unknown) { + return 1 === schema.oneOf.reduce((acc: number, schema: any) => (Value.Check(schema, value) ? acc + 1 : acc), 0) + } + if (!TypeRegistry.Has('UnionOneOf')) TypeRegistry.Set('UnionOneOf', UnionOneOfCheck) + return { ...options, [Kind]: 'UnionOneOf', oneOf } as TUnionOneOf +} \ No newline at end of file diff --git a/example/standard/index.ts b/example/standard/index.ts new file mode 100644 index 000000000..331f30aa4 --- /dev/null +++ b/example/standard/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/standard-schema + +The MIT License (MIT) + +Copyright (c) 2024 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './standard' \ No newline at end of file diff --git a/example/standard/readme.md b/example/standard/readme.md new file mode 100644 index 000000000..6dc310269 --- /dev/null +++ b/example/standard/readme.md @@ -0,0 +1,33 @@ +### Standard Schema + +Reference implementation of [Standard Schema](https://github.com/standard-schema/standard-schema) for TypeBox. + +### Example + +The following example augments a TypeBox schema with the required `~standard` interface. The `~standard` interface is applied via non-enumerable configuration enabling the schematics to continue to be used with strict compliant validators such as Ajv that would otherwise reject the non-standard `~standard` keyword. + +```typescript +import { StandardSchema } from './standard' +import { Type } from '@sinclair/typebox' + +const T = StandardSchema(Type.Object({ // const A = { + x: Type.Number(), // (non-enumerable) '~standard': { + y: Type.Number(), // version: 1, + z: Type.Number(), // vendor: 'TypeBox', +})) // validate: [Function: validate] + // }, + // type: 'object', + // properties: { + // x: { type: 'number', [Symbol(TypeBox.Kind)]: 'Number' }, + // y: { type: 'number', [Symbol(TypeBox.Kind)]: 'Number' }, + // z: { type: 'number', [Symbol(TypeBox.Kind)]: 'Number' } + // }, + // required: [ 'x', 'y', 'z' ], + // [Symbol(TypeBox.Kind)]: 'Object' + // } + +const R = T['~standard'].validate({ x: 1, y: 2, z: 3 }) // const R = { + // value: { x: 1, y: 2, z: 3 }, + // issues: [] + // } +``` \ No newline at end of file diff --git a/example/standard/standard.ts b/example/standard/standard.ts new file mode 100644 index 000000000..ef5ca384e --- /dev/null +++ b/example/standard/standard.ts @@ -0,0 +1,85 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/standard-schema + +The MIT License (MIT) + +Copyright (c) 2024 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { AssertError, Value, ValueError, ValueErrorType } from '@sinclair/typebox/value' +import { TSchema, StaticDecode, CloneType } from '@sinclair/typebox' + +// ------------------------------------------------------------------ +// StandardSchema +// ------------------------------------------------------------------ +interface StandardResult { + value: Output + issues: ValueError[] +} +interface StandardSchema { + readonly "~standard": StandardSchemaProperties +} +interface StandardSchemaProperties { + readonly version: 1 + readonly vendor: 'TypeBox' + readonly validate: (value: unknown) => StandardResult + readonly types?: undefined +} +// ------------------------------------------------------------------ +// Issues +// ------------------------------------------------------------------ +// prettier-ignore +function CreateIssues(schema: TSchema, value: unknown, error: unknown): ValueError[] { + const isAssertError = error instanceof AssertError ? error : undefined + return !isAssertError + ? [{errors: [], message: 'Unknown error', path: '/', type: ValueErrorType.Kind, schema, value }] + : [...isAssertError.Errors()] +} +// ------------------------------------------------------------------ +// Validate +// ------------------------------------------------------------------ +// prettier-ignore +function CreateValidator(schema: Type, references: TSchema[]): (value: unknown) => StandardResult> { + return (value: unknown): StandardResult> => { + try { + return { value: Value.Parse(schema, references, value), issues: [] } + } catch (error) { + return { value: undefined, issues: CreateIssues(schema, value, error) } + } + } +} +// ------------------------------------------------------------------ +// StandardSchema +// ------------------------------------------------------------------ +/** Augments a TypeBox type with the `~standard` validation interface. */ +export type TStandardSchema = ( + Input & StandardSchema +) +/** Augments a TypeBox type with the `~standard` validation interface. */ +export function StandardSchema(schema: Type, references: TSchema[] = []): TStandardSchema> { + const standard = { version: 1, vendor: 'TypeBox', validate: CreateValidator(schema, references) } + return Object.defineProperty(CloneType(schema), "~standard", { + enumerable: false, + value: standard + }) as never +} \ No newline at end of file diff --git a/example/typedef/index.ts b/example/typedef/index.ts new file mode 100644 index 000000000..daac6020a --- /dev/null +++ b/example/typedef/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/typedef + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './typedef' \ No newline at end of file diff --git a/example/typedef/readme.md b/example/typedef/readme.md new file mode 100644 index 000000000..80639ff02 --- /dev/null +++ b/example/typedef/readme.md @@ -0,0 +1,266 @@ +# TypeDef + +TypeBox is considering support for the JSON Type Definition [RFC8927](https://www.rfc-editor.org/rfc/rfc8927) specification in future releases. This specification is similar to JSON Schema but provides a constrained type representation that enables schematics to map more naturally to [nominal type systems](https://en.wikipedia.org/wiki/Nominal_type_system) as well as offering type primitives such as `int8`, `uint32` or `float32`. JSON Type Definition can be useful in applications that need to express and share data structures in a way that can be understood by a wide range of programming languages outside of JavaScript. + +License MIT + +## Contents +- [Usage](#Usage) +- [Types](#Types) +- [Unions](#Unions) +- [Check](#Check) + +## Usage + +TypeBox currently doesn't publish TypeDef as part of the mainline package. However the TypeDef functionality is written to be a standalone module you can copy into your project. You will also need `@sinclair/typebox` installed. You can obtain the `typedef` module from `example/typedef/typedef.ts` contained within this repository. + +```typescript +import { Type, Static } from './typedef' + +const T = Type.Struct({ // const T = { + x: Type.Float32(), // properties: { + y: Type.Float32(), // x: { type: 'float32' }, + z: Type.Float32() // y: { type: 'float32' }, +}) // z: { type: 'float32' } + // } + // } + +type T = Static // type T = { + // x: number, + // y: number, + // z: number + // } +``` + +## Types + +The following types are supported by the typedef module. Please note these types are not compatible with the JSON Schema specification and should not be combined with the standard TypeBox types. + +```typescript +┌────────────────────────────────┬─────────────────────────────┬────────────────────────────────┐ +│ TypeBox │ TypeScript │ JSON Type Definition │ +│ │ │ │ +├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ +│ const T = Type.Boolean() │ type T = boolean │ const T = { │ +│ │ │ type: 'boolean' │ +│ │ │ } │ +├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ +│ const T = Type.String() │ type T = string │ const T = { │ +│ │ │ type: 'string' │ +│ │ │ } │ +│ │ │ │ +├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ +│ const T = Type.Float32() │ type T = number │ const T = { │ +│ │ │ type: 'float32' │ +│ │ │ } │ +│ │ │ │ +├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ +│ const T = Type.Float64() │ type T = number │ const T = { │ +│ │ │ type: 'float64' │ +│ │ │ } │ +│ │ │ │ +├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ +│ const T = Type.Int8() │ type T = number │ const T = { │ +│ │ │ type: 'int8' │ +│ │ │ } │ +│ │ │ │ +├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ +│ const T = Type.Int16() │ type T = number │ const T = { │ +│ │ │ type: 'int16' │ +│ │ │ } │ +│ │ │ │ +├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ +│ const T = Type.Int32() │ type T = number │ const T = { │ +│ │ │ type: 'int32' │ +│ │ │ } │ +│ │ │ │ +├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ +│ const T = Type.Uint8() │ type T = number │ const T = { │ +│ │ │ type: 'uint8' │ +│ │ │ } │ +│ │ │ │ +├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ +│ const T = Type.Uint16() │ type T = number │ const T = { │ +│ │ │ type: 'uint16' │ +│ │ │ } │ +│ │ │ │ +├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ +│ const T = Type.Uint32() │ type T = number │ const T = { │ +│ │ │ type: 'uint32' │ +│ │ │ } │ +│ │ │ │ +├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ +│ const T = Type.Timestamp() │ type T = number │ const T = { │ +│ │ │ type: 'timestamp' │ +│ │ │ } │ +│ │ │ │ +├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ +│ const T = Type.Struct([ │ type T = { │ const T = { │ +│ x: Type.Float32(), │ x: number, │ properties: { │ +│ y: Type.Float32(), │ y: number │ x: number, │ +│ ]) │ } │ y: number │ +│ │ │ } │ +│ │ │ } │ +│ │ │ │ +├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ +│ const T = Type.Array( │ type T = number[] │ const T = { │ +│ Type.Float32() │ │ elements: { │ +│ ) │ │ type: 'float32' │ +│ │ │ } │ +│ │ │ } │ +│ │ │ │ +├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ +│ const T = Type.Record( │ type T = Record< │ const T = { │ +│ Type.Float32() │ string, │ values: { │ +│ ) │ number │ type: 'float32' │ +│ │ > │ } │ +│ │ │ } │ +│ │ │ │ +├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ +│ const T = Type.Enum([ │ type T = 'A' | 'B' | 'C' │ const T = { │ +│ 'A', 'B', 'C' │ │ enum: [ │ +│ ]) │ │ 'A', │ +│ │ │ 'B', │ +│ │ │ 'C' │ +│ │ │ ] │ +│ │ │ } │ +│ │ │ │ +├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ +│ const T = Type.Union([ │ type T = { │ const T = { │ +│ Type.Struct({ │ kind: '0', │ discriminator: 'kind', │ +│ x: Type.Float32() │ x: number │ mapping: { │ +│ }), │ } | { │ '0': { │ +│ Type.Struct({ │ kind: '1' │ properties: { │ +│ y: Type.Float32() │ y: number │ x: { │ +│ ]) │ } │ type: 'float32' │ +│ ], 'kind') │ │ } │ +│ │ │ } │ +│ │ │ }, │ +│ │ │ '1': { | +│ │ │ properties: { │ +│ │ │ y: { │ +│ │ │ type: 'float32' │ +│ │ │ } │ +│ │ │ } │ +│ │ │ } │ +│ │ │ } │ +│ │ │ } │ +│ │ │ │ +└────────────────────────────────┴─────────────────────────────┴────────────────────────────────┘ +``` + +## Unions + +TypeBox supports JSON Type Definition discriminated unions with `Type.Union`. This type works similar its JSON Schema counterpart, but can only accept types of `Type.Struct` and will infer each struct with an additional named `discriminator` field. The representation for discriminated unions are also quite different, where instead of `anyOf` or `oneOf`, a set of `mapping` properties are used for each sub type. + +```typescript +const Vector2 = Type.Struct({ // const Vector2 = { + x: Type.Float32(), // properties: { + y: Type.Float32() // x: { type: 'float32' }, +}) // y: { type: 'float32' } + // } + // } + +const Vector3 = Type.Struct({ // const Vector3 = { + x: Type.Float32(), // properties: { + y: Type.Float32(), // x: { type: 'float32' }, + z: Type.Float32() // y: { type: 'float32' }, +}) // z: { type: 'float32' } + // } + // } + +const Vector4 = Type.Struct({ // const Vector4 = { + x: Type.Float32(), // properties: { + y: Type.Float32(), // x: { type: 'float32' }, + z: Type.Float32(), // y: { type: 'float32' }, + w: Type.Float32() // z: { type: 'float32' }, +}) // w: { type: 'float32' } + // } + // } + +const T = Type.Union([ // const T = { + Vector2, // discriminator: 'type', + Vector3, // mapping: { + Vector4 // 0: { +]) // properties: { + // x: { type: 'float32' }, + // y: { type: 'float32' } + // } + // }, + // 1: { + // properties: { + // x: { type: 'float32' }, + // y: { type: 'float32' }, + // z: { type: 'float32' } + // } + // }, + // 2: { + // properties: { + // x: { type: 'float32' }, + // y: { type: 'float32' }, + // z: { type: 'float32' } + // } + // } + // } + // } + + +type T = Static // type T = { + // type: '0', + // x: number, + // y: number + // } | { + // type: '1', + // x: number, + // y: number, + // y: number + // } | { + // type: '2', + // x: number, + // y: number, + // y: number, + // w: number + // } +``` +To type check a value matching the above union, the value will need to contain the discriminator property `type` with a value matching one of the sub type `mapping` keys. The inference type shown above can be a good reference point to understand the structure of the expected value. Nominal type systems will use the discriminator to an expected target type. + +The following are examples of valid and invalid union data. + +```typescript + +const V = { x: 1, y: 1 } // invalid Vector2 + +const V = { type: '0', x: 1, y: 1 } // valid Vector2 + +const V = { type: '0', x: 1, y: 1, z: 1 } // invalid Vector2 + +const V = { type: '1', x: 1, y: 1, z: 1 } // valid Vector3 +``` + + +## Check + +TypeDef types are partially supported with the `TypeCompiler` and `Value` checking modules through the extensible type system in TypeBox. Please note these types are not optimized for JIT performance and do not provide deep error reporting support. For more fully featured validation support consider Ajv. Documentation of Ajv support can be found [here](https://ajv.js.org/json-type-definition.html). + +The following is TypeDef used with TypeBox's type checking infrastructure. + +```typescript +import { TypeCompiler } from '@sinclair/typebox/compiler' +import { Value } from '@sinclair/typebox/value' + +const T = Type.Struct({ + x: Type.Float32(), + y: Type.Float32(), + z: Type.Float32() +}) + +const V = { + x: 1, + y: 2, + z: 3 +} + +const R1 = TypeCompiler.Compile(T).Check(V) // true + +const R2 = Value.Check(T, V) // true +``` \ No newline at end of file diff --git a/example/typedef/typedef.ts b/example/typedef/typedef.ts new file mode 100644 index 000000000..f0b74da5f --- /dev/null +++ b/example/typedef/typedef.ts @@ -0,0 +1,619 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/typedef + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { SetErrorFunction, DefaultErrorFunction } from '@sinclair/typebox/errors' +import * as Types from '@sinclair/typebox/type' + +// -------------------------------------------------------------------------- +// Metadata +// -------------------------------------------------------------------------- +export interface Metadata { + [name: string]: any +} +// -------------------------------------------------------------------------- +// TArray +// -------------------------------------------------------------------------- +export interface TArray extends Types.TSchema { + [Types.Kind]: 'TypeDef:Array' + static: Types.Static[] + elements: T +} +// -------------------------------------------------------------------------- +// TBoolean +// -------------------------------------------------------------------------- +export interface TBoolean extends Types.TSchema { + [Types.Kind]: 'TypeDef:Boolean' + static: 'boolean' + type: 'boolean' +} +// -------------------------------------------------------------------------- +// TUnion +// -------------------------------------------------------------------------- +export type InferUnion = + T extends [infer L extends TStruct, ...infer R extends TStruct[]] + ? Types.Evaluate<{ [_ in D]: Index } & Types.Static> | InferUnion>> + : never + +export interface TUnion extends Types.TSchema { + [Types.Kind]: 'TypeDef:Union' + static: InferUnion + discriminator: D, + mapping: T +} +// -------------------------------------------------------------------------- +// TEnum +// -------------------------------------------------------------------------- +export interface TEnum extends Types.TSchema { + [Types.Kind]: 'TypeDef:Enum' + static: T[number] + enum: [...T] +} +// -------------------------------------------------------------------------- +// TFloat32 +// -------------------------------------------------------------------------- +export interface TFloat32 extends Types.TSchema { + [Types.Kind]: 'TypeDef:Float32' + type: 'float32' + static: number +} +// -------------------------------------------------------------------------- +// TFloat64 +// -------------------------------------------------------------------------- +export interface TFloat64 extends Types.TSchema { + [Types.Kind]: 'TypeDef:Float64' + type: 'float64' + static: number +} +// -------------------------------------------------------------------------- +// TInt8 +// -------------------------------------------------------------------------- +export interface TInt8 extends Types.TSchema { + [Types.Kind]: 'TypeDef:Int8' + type: 'int8' + static: number +} +// -------------------------------------------------------------------------- +// TInt16 +// -------------------------------------------------------------------------- +export interface TInt16 extends Types.TSchema { + [Types.Kind]: 'TypeDef:Int16' + type: 'int16' + static: number +} +// -------------------------------------------------------------------------- +// TInt32 +// -------------------------------------------------------------------------- +export interface TInt32 extends Types.TSchema { + [Types.Kind]: 'TypeDef:Int32' + type: 'int32' + static: number +} +// -------------------------------------------------------------------------- +// TUint8 +// -------------------------------------------------------------------------- +export interface TUint8 extends Types.TSchema { + [Types.Kind]: 'TypeDef:Uint8' + type: 'uint8' + static: number +} +// -------------------------------------------------------------------------- +// TUint16 +// -------------------------------------------------------------------------- +export interface TUint16 extends Types.TSchema { + [Types.Kind]: 'TypeDef:Uint16' + type: 'uint16' + static: number +} +// -------------------------------------------------------------------------- +// TUint32 +// -------------------------------------------------------------------------- +export interface TUint32 extends Types.TSchema { + [Types.Kind]: 'TypeDef:Uint32' + type: 'uint32' + static: number +} +// -------------------------------------------------------------------------- +// TProperties +// -------------------------------------------------------------------------- +export type TFields = Record +// -------------------------------------------------------------------------- +// TRecord +// -------------------------------------------------------------------------- +export interface TRecord extends Types.TSchema { + [Types.Kind]: 'TypeDef:Record' + static: Record> + values: T +} +// -------------------------------------------------------------------------- +// TString +// -------------------------------------------------------------------------- +export interface TString extends Types.TSchema { + [Types.Kind]: 'TypeDef:String' + type: 'string' + static: string +} +// -------------------------------------------------------------------------- +// TStruct +// -------------------------------------------------------------------------- +// used for structural type inference +type OptionalKeys = { [K in keyof T]: T[K] extends (Types.TOptional) ? T[K] : never } +type RequiredKeys = { [K in keyof T]: T[K] extends (Types.TOptional) ? never : T[K] } +// static inference +type ReadonlyOptionalPropertyKeys = { [K in keyof T]: T[K] extends Types.TReadonly ? (T[K] extends Types.TOptional ? K : never) : never }[keyof T] +type ReadonlyPropertyKeys = { [K in keyof T]: T[K] extends Types.TReadonly ? (T[K] extends Types.TOptional ? never : K) : never }[keyof T] +type OptionalPropertyKeys = { [K in keyof T]: T[K] extends Types.TOptional ? (T[K] extends Types.TReadonly ? never : K) : never }[keyof T] +type RequiredPropertyKeys = keyof Omit | ReadonlyPropertyKeys | OptionalPropertyKeys> +// prettier-ignore +type StructStaticProperties> = Types.Evaluate<( + Readonly>>> & + Readonly>> & + Partial>> & + Required>> +)> +// prettier-ignore +export type StructStatic = StructStaticProperties +}> +export interface StructMetadata extends Metadata { + additionalProperties?: boolean +} +export interface TStruct extends Types.TSchema, StructMetadata { + [Types.Kind]: 'TypeDef:Struct' + static: StructStatic + optionalProperties: { [K in Types.Assert, keyof T>]: T[K] } + properties: { [K in Types.Assert, keyof T>]: T[K] } +} +// -------------------------------------------------------------------------- +// TTimestamp +// -------------------------------------------------------------------------- +export interface TTimestamp extends Types.TSchema { + [Types.Kind]: 'TypeDef:Timestamp' + type: 'timestamp' + static: string +} +// -------------------------------------------------------------------------- +// Static +// -------------------------------------------------------------------------- +export type Static = Types.Static + +// -------------------------------------------------------------------------- +// TimestampFormat +// -------------------------------------------------------------------------- +export namespace TimestampFormat { + const DATE_TIME_SEPARATOR = /t|\s/i + const TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i + const DATE = /^(\d\d\d\d)-(\d\d)-(\d\d)$/ + const DAYS = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + function IsLeapYear(year: number): boolean { + return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) + } + function IsDate(str: string): boolean { + const matches: string[] | null = DATE.exec(str) + if (!matches) return false + const year: number = +matches[1] + const month: number = +matches[2] + const day: number = +matches[3] + return month >= 1 && month <= 12 && day >= 1 && day <= (month === 2 && IsLeapYear(year) ? 29 : DAYS[month]) + } + function IsTime(str: string, strictTimeZone?: boolean): boolean { + const matches: string[] | null = TIME.exec(str) + if (!matches) return false + const hr: number = +matches[1] + const min: number = +matches[2] + const sec: number = +matches[3] + const tz: string | undefined = matches[4] + const tzSign: number = matches[5] === '-' ? -1 : 1 + const tzH: number = +(matches[6] || 0) + const tzM: number = +(matches[7] || 0) + if (tzH > 23 || tzM > 59 || (strictTimeZone && !tz)) return false + if (hr <= 23 && min <= 59 && sec < 60) return true + const utcMin = min - tzM * tzSign + const utcHr = hr - tzH * tzSign - (utcMin < 0 ? 1 : 0) + return (utcHr === 23 || utcHr === -1) && (utcMin === 59 || utcMin === -1) && sec < 61 + } + function IsDateTime(value: string, strictTimeZone?: boolean): boolean { + const dateTime: string[] = value.split(DATE_TIME_SEPARATOR) + return dateTime.length === 2 && IsDate(dateTime[0]) && IsTime(dateTime[1], strictTimeZone) + } + export function Check(value: string): boolean { + return IsDateTime(value) + } +} +// -------------------------------------------------------------------------- +// ValueCheck +// -------------------------------------------------------------------------- +export class ValueCheckError extends Types.TypeBoxError { + constructor(public readonly schema: Types.TSchema) { + super('Unknown type') + } +} +export namespace ValueCheck { + // ------------------------------------------------------------------------ + // Guards + // ------------------------------------------------------------------------ + function IsObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !globalThis.Array.isArray(value) + } + function IsArray(value: unknown): value is unknown[] { + return globalThis.Array.isArray(value) + } + function IsString(value: unknown): value is string { + return typeof value === 'string' + } + function IsInt(value: unknown, min: number, max: number): value is number { + return typeof value === 'number' && globalThis.Number.isInteger(value) && value >= min && value < max + } + // ------------------------------------------------------------------------ + // Types + // ------------------------------------------------------------------------ + function Array(schema: TArray, value: unknown): boolean { + return IsArray(value) && value.every(value => Visit(schema.elements, value)) + } + function Boolean(schema: TBoolean, value: unknown): boolean { + return typeof value === 'boolean' + } + function Enum(schema: TEnum, value: unknown): boolean { + return typeof value === 'string' && schema.enum.includes(value) + } + function Float32(schema: TFloat32, value: unknown): boolean { + return typeof value === 'number' + } + function Float64(schema: TFloat64, value: unknown): boolean { + return typeof value === 'number' + } + function Int8(schema: TInt8, value: unknown): boolean { + return IsInt(value, -128, 127) + } + function Int16(schema: TInt16, value: unknown): boolean { + return IsInt(value, -32_768, 32_767) + } + function Int32(schema: TInt32, value: unknown): boolean { + return IsInt(value, -2_147_483_648, 2_147_483_647) + } + function Uint8(schema: TUint8, value: unknown): boolean { + return IsInt(value, 0, 255) + } + function Uint16(schema: TUint16, value: unknown): boolean { + return IsInt(value, 0, 65535) + } + function Uint32(schema: TUint32, value: unknown): boolean { + return IsInt(value, 0, 4_294_967_295) + } + function Record(schema: TRecord, value: unknown): boolean { + return IsObject(value) && globalThis.Object.getOwnPropertyNames(value).every(key => Visit(schema.values, value[key])) + } + function String(schema: TString, value: unknown): boolean { + return typeof value === 'string' + } + function Struct(schema: TStruct, value: unknown, descriminator?: string): boolean { + if (!IsObject(value)) return false + const optionalKeys = schema.optionalProperties === undefined ? [] : globalThis.Object.getOwnPropertyNames(schema.optionalProperties) + const requiredKeys = schema.properties === undefined ? [] : globalThis.Object.getOwnPropertyNames(schema.properties) + const unknownKeys = globalThis.Object.getOwnPropertyNames(value) + for (const requiredKey of requiredKeys) { + if (!(requiredKey in value)) return false + const requiredProperty = value[requiredKey] + const requiredSchema = (schema as any).properties[requiredKey] + if (!Visit(requiredSchema, requiredProperty)) return false + } + for (const optionalKey of optionalKeys) { + if (!(optionalKey in value)) continue + const optionalProperty = value[optionalKey] + const optionalSchema = (schema as any).properties[optionalKey] + if (!Visit(optionalSchema, optionalProperty)) return false + } + if (schema.additionalProperties === true) return true + const knownKeys = [...optionalKeys, ...requiredKeys] + for (const unknownKey of unknownKeys) if (!knownKeys.includes(unknownKey) && (descriminator !== undefined && unknownKey !== descriminator)) return false + for (const knownKey of knownKeys) if (!unknownKeys.includes(knownKey)) return false + return true + } + function Timestamp(schema: TString, value: unknown): boolean { + return IsString(value) && TimestampFormat.Check(value) + } + function Union(schema: TUnion, value: unknown): boolean { + if (!IsObject(value)) return false + if (!(schema.discriminator in value)) return false + if (!IsString(value[schema.discriminator])) return false + if (!(value[schema.discriminator] in schema.mapping)) return false + const struct = schema.mapping[value[schema.discriminator]] as TStruct + return Struct(struct, value, schema.discriminator) + } + function Visit(schema: Types.TSchema, value: unknown): boolean { + const anySchema = schema as any + switch (anySchema[Types.Kind]) { + case 'TypeDef:Array': return Array(anySchema, value) + case 'TypeDef:Boolean': return Boolean(anySchema, value) + case 'TypeDef:Union': return Union(anySchema, value) + case 'TypeDef:Enum': return Enum(anySchema, value) + case 'TypeDef:Float32': return Float32(anySchema, value) + case 'TypeDef:Float64': return Float64(anySchema, value) + case 'TypeDef:Int8': return Int8(anySchema, value) + case 'TypeDef:Int16': return Int16(anySchema, value) + case 'TypeDef:Int32': return Int32(anySchema, value) + case 'TypeDef:Uint8': return Uint8(anySchema, value) + case 'TypeDef:Uint16': return Uint16(anySchema, value) + case 'TypeDef:Uint32': return Uint32(anySchema, value) + case 'TypeDef:Record': return Record(anySchema, value) + case 'TypeDef:String': return String(anySchema, value) + case 'TypeDef:Struct': return Struct(anySchema, value) + case 'TypeDef:Timestamp': return Timestamp(anySchema, value) + default: throw new ValueCheckError(anySchema) + } + } + export function Check(schema: T, value: unknown): value is Types.Static { + return Visit(schema, value) + } +} +// -------------------------------------------------------------------------- +// TypeGuard +// -------------------------------------------------------------------------- +export namespace TypeGuard { + // ------------------------------------------------------------------------ + // Guards + // ------------------------------------------------------------------------ + function IsObject(value: unknown): value is Record { + return typeof value === 'object' + } + function IsArray(value: unknown): value is unknown[] { + return globalThis.Array.isArray(value) + } + function IsOptionalBoolean(value: unknown): value is boolean | undefined { + return IsBoolean(value) || value === undefined + } + function IsBoolean(value: unknown): value is boolean { + return typeof value === 'boolean' + } + function IsString(value: unknown): value is string { + return typeof value === 'string' + } + // ------------------------------------------------------------------------ + // Types + // ------------------------------------------------------------------------ + export function TArray(schema: unknown): schema is TArray { + return IsObject(schema) && schema[Types.Kind] === 'TypeDef:Array' && TSchema(schema['elements']) + } + export function TBoolean(schema: unknown): schema is TBoolean { + return IsObject(schema) && schema[Types.Kind] === 'TypeDef:Boolean' && schema['type'] === 'boolean' + } + export function TUnion(schema: unknown): schema is TUnion { + if(!(IsObject(schema) && schema[Types.Kind] === 'TypeDef:Union' && IsString(schema['discriminator']) && IsObject(schema['mapping']))) return false + return globalThis.Object.getOwnPropertyNames(schema['mapping']).every(key => TSchema((schema['mapping'] as any)[key])) + } + export function TEnum(schema: unknown): schema is TEnum { + return IsObject(schema) && schema[Types.Kind] === 'TypeDef:Enum' && IsArray(schema['enum']) && schema['enum'].every(item => IsString(item)) + } + export function TFloat32(schema: unknown): schema is TFloat32 { + return IsObject(schema) && schema[Types.Kind] === 'TypeDef:Float32' && schema['type'] === 'float32' + } + export function TFloat64(schema: unknown): schema is TFloat64 { + return IsObject(schema) && schema[Types.Kind] === 'TypeDef:Float64' && schema['type'] === 'float64' + } + export function TInt8(schema: unknown): schema is TInt8 { + return IsObject(schema) && schema[Types.Kind] === 'TypeDef:Int8' && schema['type'] === 'int8' + } + export function TInt16(schema: unknown): schema is TInt16 { + return IsObject(schema) && schema[Types.Kind] === 'TypeDef:Int16' && schema['type'] === 'int16' + } + export function TInt32(schema: unknown): schema is TInt32 { + return IsObject(schema) && schema[Types.Kind] === 'TypeDef:Int32' && schema['type'] === 'int32' + } + export function TUint8(schema: unknown): schema is TUint8 { + return IsObject(schema) && schema[Types.Kind] === 'TypeDef:Uint8' && schema['type'] === 'uint8' + } + export function TUint16(schema: unknown): schema is TUint16 { + return IsObject(schema) && schema[Types.Kind] === 'TypeDef:Uint16' && schema['type'] === 'uint16' + } + export function TUint32(schema: unknown): schema is TUint32 { + return IsObject(schema) && schema[Types.Kind] === 'TypeDef:Uint32' && schema['type'] === 'uint32' + } + export function TRecord(schema: unknown): schema is TRecord { + return IsObject(schema) && schema[Types.Kind] === 'TypeDef:Record' && TSchema(schema['values']) + } + export function TString(schema: unknown): schema is TString { + return IsObject(schema) && schema[Types.Kind] === 'TypeDef:String' && schema['type'] === 'string' + } + export function TStruct(schema: unknown): schema is TStruct { + if(!(IsObject(schema) && schema[Types.Kind] === 'TypeDef:Struct' && IsOptionalBoolean(schema['additionalProperties']))) return false + const optionalProperties = schema['optionalProperties'] + const requiredProperties = schema['properties'] + const optionalCheck = optionalProperties === undefined || IsObject(optionalProperties) && globalThis.Object.getOwnPropertyNames(optionalProperties).every(key => TSchema(optionalProperties[key])) + const requiredCheck = requiredProperties === undefined || IsObject(requiredProperties) && globalThis.Object.getOwnPropertyNames(requiredProperties).every(key => TSchema(requiredProperties[key])) + return optionalCheck && requiredCheck + } + export function TTimestamp(schema: unknown): schema is TTimestamp { + return IsObject(schema) && schema[Types.Kind] === 'TypeDef:Timestamp' && schema['type'] === 'timestamp' + } + export function TKind(schema: unknown): schema is Types.TKind { + return IsObject(schema) && Types.Kind in schema && typeof (schema as any)[Types.Kind] === 'string' // TS 4.1.5: any required for symbol indexer + } + export function TSchema(schema: unknown): schema is Types.TSchema { + // prettier-ignore + return ( + TArray(schema) || + TBoolean(schema) || + TUnion(schema) || + TEnum(schema) || + TFloat32(schema) || + TFloat64(schema) || + TInt8(schema) || + TInt16(schema) || + TInt32(schema) || + TUint8(schema) || + TUint16(schema) || + TUint32(schema) || + TRecord(schema) || + TString(schema) || + TStruct(schema) || + TTimestamp(schema) || + (TKind(schema) && Types.TypeRegistry.Has(schema[Types.Kind])) + ) + } +} +// -------------------------------------------------------------------------- +// TypeRegistry +// -------------------------------------------------------------------------- +Types.TypeRegistry.Set('TypeDef:Array', (schema, value) => ValueCheck.Check(schema, value)) +Types.TypeRegistry.Set('TypeDef:Boolean', (schema, value) => ValueCheck.Check(schema, value)) +Types.TypeRegistry.Set('TypeDef:Union', (schema, value) => ValueCheck.Check(schema, value)) +Types.TypeRegistry.Set('TypeDef:Int8', (schema, value) => ValueCheck.Check(schema, value)) +Types.TypeRegistry.Set('TypeDef:Int16', (schema, value) => ValueCheck.Check(schema, value)) +Types.TypeRegistry.Set('TypeDef:Int32', (schema, value) => ValueCheck.Check(schema, value)) +Types.TypeRegistry.Set('TypeDef:Uint8', (schema, value) => ValueCheck.Check(schema, value)) +Types.TypeRegistry.Set('TypeDef:Uint16', (schema, value) => ValueCheck.Check(schema, value)) +Types.TypeRegistry.Set('TypeDef:Uint32', (schema, value) => ValueCheck.Check(schema, value)) +Types.TypeRegistry.Set('TypeDef:Record', (schema, value) => ValueCheck.Check(schema, value)) +Types.TypeRegistry.Set('TypeDef:String', (schema, value) => ValueCheck.Check(schema, value)) +Types.TypeRegistry.Set('TypeDef:Struct', (schema, value) => ValueCheck.Check(schema, value)) +Types.TypeRegistry.Set('TypeDef:Timestamp', (schema, value) => ValueCheck.Check(schema, value)) +// -------------------------------------------------------------------------- +// TypeSystemErrorFunction +// -------------------------------------------------------------------------- +SetErrorFunction((error) => { + switch(error.schema[Types.Kind]) { + case 'TypeDef:Array': return 'Expected Array' + case 'TypeDef:Boolean': return 'Expected Boolean' + case 'TypeDef:Union': return 'Expected Union' + case 'TypeDef:Int8': return 'Expected Int8' + case 'TypeDef:Int16': return 'Expected Int16' + case 'TypeDef:Int32': return 'Expected Int32' + case 'TypeDef:Uint8': return 'Expected Uint8' + case 'TypeDef:Uint16': return 'Expected Uint16' + case 'TypeDef:Uint32': return 'Expected Uint32' + case 'TypeDef:Record': return 'Expected Record' + case 'TypeDef:String': return 'Expected String' + case 'TypeDef:Struct': return 'Expected Struct' + case 'TypeDef:Timestamp': return 'Expected Timestamp' + } + return DefaultErrorFunction(error) +}) +// -------------------------------------------------------------------------- +// TypeDefBuilder +// -------------------------------------------------------------------------- +export class TypeDefBuilder { + // ------------------------------------------------------------------------ + // Core + // ------------------------------------------------------------------------ + protected Create(schema: Record, metadata: Record): any { + const keys = globalThis.Object.getOwnPropertyNames(metadata) + return keys.length > 0 ? { ...schema, metadata: { ...metadata } } : { ...schema } + } + /** [Standard] Removes compositing symbols from this schema */ + public Strict(schema: T): T { + return JSON.parse(JSON.stringify(schema)) as T + } + // ------------------------------------------------------------------------ + // Modifiers + // ------------------------------------------------------------------------ + /** `[Standard]` Creates an Optional property */ + public Optional(schema: T): Types.TOptional { + return this.Optional(schema) + } + /** `[Standard]` Creates a Readonly property */ + public Readonly(schema: T): Types.TReadonly { + return this.Readonly(schema) + } + // ------------------------------------------------------------------------ + // Types + // ------------------------------------------------------------------------ + /** [Standard] Creates a Array type */ + public Array(elements: T, metadata: Metadata = {}): TArray { + return this.Create({ [Types.Kind]: 'TypeDef:Array', elements }, metadata) + } + /** [Standard] Creates a Boolean type */ + public Boolean(metadata: Metadata = {}): TBoolean { + return this.Create({ [Types.Kind]: 'TypeDef:Boolean', type: 'boolean' }, metadata) + } + /** [Standard] Creates a Enum type */ + public Enum(values: [...T], metadata: Metadata = {}): TEnum { + return this.Create({[Types.Kind]: 'TypeDef:Enum', enum: values }, metadata ) + } + /** [Standard] Creates a Float32 type */ + public Float32(metadata: Metadata = {}): TFloat32 { + return this.Create({ [Types.Kind]: 'TypeDef:Float32', type: 'float32' }, metadata) + } + /** [Standard] Creates a Float64 type */ + public Float64(metadata: Metadata = {}): TFloat64 { + return this.Create({ [Types.Kind]: 'TypeDef:Float64', type: 'float64' }, metadata) + } + /** [Standard] Creates a Int8 type */ + public Int8(metadata: Metadata = {}): TInt8 { + return this.Create({ [Types.Kind]: 'TypeDef:Int8', type: 'int8' }, metadata) + } + /** [Standard] Creates a Int16 type */ + public Int16(metadata: Metadata = {}): TInt16 { + return this.Create({ [Types.Kind]: 'TypeDef:Int16', type: 'int16' }, metadata) + } + /** [Standard] Creates a Int32 type */ + public Int32(metadata: Metadata = {}): TInt32 { + return this.Create({ [Types.Kind]: 'TypeDef:Int32', type: 'int32' }, metadata) + } + /** [Standard] Creates a Uint8 type */ + public Uint8(metadata: Metadata = {}): TUint8 { + return this.Create({ [Types.Kind]: 'TypeDef:Uint8', type: 'uint8' }, metadata) + } + /** [Standard] Creates a Uint16 type */ + public Uint16(metadata: Metadata = {}): TUint16 { + return this.Create({ [Types.Kind]: 'TypeDef:Uint16', type: 'uint16' }, metadata) + } + /** [Standard] Creates a Uint32 type */ + public Uint32(metadata: Metadata = {}): TUint32 { + return this.Create({ [Types.Kind]: 'TypeDef:Uint32', type: 'uint32' }, metadata) + } + /** [Standard] Creates a Record type */ + public Record(values: T, metadata: Metadata = {}): TRecord { + return this.Create({ [Types.Kind]: 'TypeDef:Record', values },metadata) + } + /** [Standard] Creates a String type */ + public String(metadata: Metadata = {}): TString { + return this.Create({ [Types.Kind]: 'TypeDef:String', type: 'string' }, metadata) + } + /** [Standard] Creates a Struct type */ + public Struct(fields: T, metadata: StructMetadata = {}): TStruct { + const optionalProperties = globalThis.Object.getOwnPropertyNames(fields).reduce((acc, key) => (Types.TypeGuard.IsOptional(fields[key]) ? { ...acc, [key]: fields[key] } : { ...acc }), {} as TFields) + const properties = globalThis.Object.getOwnPropertyNames(fields).reduce((acc, key) => (Types.TypeGuard.IsOptional(fields[key]) ? { ...acc } : { ...acc, [key]: fields[key] }), {} as TFields) + const optionalObject = globalThis.Object.getOwnPropertyNames(optionalProperties).length > 0 ? { optionalProperties: optionalProperties } : {} + const requiredObject = globalThis.Object.getOwnPropertyNames(properties).length === 0 ? {} : { properties: properties } + return this.Create({ [Types.Kind]: 'TypeDef:Struct', ...requiredObject, ...optionalObject }, metadata) + } + /** [Standard] Creates a Union type */ + public Union[], D extends string = 'type'>(structs: [...T], discriminator?: D): TUnion { + discriminator = (discriminator || 'type') as D + if (structs.length === 0) throw new Error('TypeDefBuilder: Union types must contain at least one struct') + const mapping = structs.reduce((acc, current, index) => ({ ...acc, [index.toString()]: current }), {}) + return this.Create({ [Types.Kind]: 'TypeDef:Union', discriminator, mapping }, {}) + } + /** [Standard] Creates a Timestamp type */ + public Timestamp(metadata: Metadata = {}): TTimestamp { + return this.Create({ [Types.Kind]: 'TypeDef:Timestamp', type: 'timestamp' }, metadata) + } +} + +/** JSON Type Definition Type Builder */ +export const Type = new TypeDefBuilder() + diff --git a/hammer.mjs b/hammer.mjs index f46ddc1b6..7ed360b62 100644 --- a/hammer.mjs +++ b/hammer.mjs @@ -1,51 +1,128 @@ +import * as Benchmark from './task/benchmark' +import * as Build from './task/build' +import * as Fs from 'fs' + // ------------------------------------------------------------------------------- // Clean // ------------------------------------------------------------------------------- - export async function clean() { - await folder('target').delete() + await folder('node_modules/typebox').delete() + await folder('target').delete() } // ------------------------------------------------------------------------------- -// Specs +// Format // ------------------------------------------------------------------------------- +export async function format() { + await shell('prettier --no-semi --single-quote --print-width 240 --trailing-comma all --write src test task example/index.ts') +} -export async function spec_types() { - await shell(`tsc -p ./src/tsconfig.json --outDir spec/types --emitDeclarationOnly`) - await shell(`tsd spec/types`) +// ------------------------------------------------------------------------------- +// Start +// ------------------------------------------------------------------------------- +export async function start() { + await shell(`hammer run example/index.ts --dist target/example`) +} +// ------------------------------------------------------------------------------- +// Benchmark +// ------------------------------------------------------------------------------- +export async function benchmark() { + await Benchmark.compression() + await Benchmark.measurement() } -export async function spec_schemas() { - await shell(`hammer build ./spec/schema/index.ts --dist target/spec/schema --platform node`) - await shell(`mocha target/spec/schema/index.js`) +// ------------------------------------------------------------------------------- +// Test +// ------------------------------------------------------------------------------- +export async function test_typescript() { + for (const version of [ + '4.9.5', '5.0.4', '5.1.3', '5.1.6', + '5.2.2', '5.3.2', '5.3.3', '5.4.3', + '5.4.5', '5.5.2', '5.5.3', '5.5.4', + '5.6.2', '5.6.3', '5.7.2', '5.7.3', + '5.8.2', '5.8.3', 'next', 'latest' + ]) { + await shell(`npm install typescript@${version} --no-save`) + await test_static() + } +} +export async function test_static() { + await shell(`tsc -v`) + await shell(`tsc -p test/static/tsconfig.json --noEmit --strict`) +} +export async function test_runtime(filter = '') { + await shell(`hammer build ./test/runtime/index.ts --dist target/test/runtime --platform node`) + await shell(`mocha target/test/runtime/index.js -g "${filter}"`) +} +export async function test(filter = '') { + await test_static() + await test_runtime(filter) } -export async function spec() { - await spec_types() - await spec_schemas() +// ------------------------------------------------------------------------------- +// Build +// ------------------------------------------------------------------------------- +export async function build_check(target = 'target/build') { + const { version } = JSON.parse(Fs.readFileSync('package.json', 'utf8')) + await shell(`cd ${target} && attw sinclair-typebox-${version}.tgz`) +} +export async function build(target = 'target/build') { + await test() + await clean() + await Promise.all([ + Build.Package.build(target), + Build.Esm.build(target), + Build.Cjs.build(target), + ]) + await folder(target).add('readme.md') + await folder(target).add('license') + await shell(`cd ${target} && npm pack`) + await build_check(target) } // ------------------------------------------------------------------------------- -// Example +// Build To // ------------------------------------------------------------------------------- -export async function example(target = 'target/example') { - await shell(`hammer run example/index.ts --dist ${target}`) +export async function build_to(remote = 'target/remote', target = 'target/build') { + await clean() + await Promise.all([ + Build.Package.build(target), + Build.Esm.build(target), + Build.Cjs.build(target), + ]) + await folder(target).add('readme.md') + await folder(target).add('license') + await shell(`cd ${target} && npm pack`) + const { version } = JSON.parse(Fs.readFileSync('package.json', 'utf8')) + const filename = `${target}/sinclair-typebox-${version}.tgz` + await folder(remote).add(filename) } // ------------------------------------------------------------------------------- -// Build +// Install // ------------------------------------------------------------------------------- +export async function install_local() { + await clean() + await build('target/typebox') + await folder('node_modules').add('target/typebox') +} -export async function build(target = 'target/build') { - await spec() - await folder(target).delete() - await shell(`tsc -p ./src/tsconfig.json --outDir ${target}`) - await folder(target).add('package.json') - 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 origin +// ------------------------------------------------------------- +// Publish +// ------------------------------------------------------------- +export async function publish(otp, target = 'target/build') { + const { version } = JSON.parse(Fs.readFileSync('package.json', 'utf8')) + if(version.includes('-dev')) throw Error(`package version should not include -dev specifier`) + await shell(`cd ${target} && npm publish sinclair-typebox-${version}.tgz --access=public --otp ${otp}`) + await shell(`git tag ${version}`) + await shell(`git push origin ${version}`) } + +// ------------------------------------------------------------- +// Publish-Dev +// ------------------------------------------------------------- +export async function publish_dev(otp, target = 'target/build') { + const { version } = JSON.parse(Fs.readFileSync(`${target}/package.json`, 'utf8')) + if(!version.includes('-dev')) throw Error(`development package version should include -dev specifier`) + await shell(`cd ${target} && npm publish sinclair-typebox-${version}.tgz --access=public --otp ${otp} --tag dev`) +} \ No newline at end of file diff --git a/license b/license index 2e5d87302..5737c52ed 100644 --- a/license +++ b/license @@ -1,8 +1,10 @@ -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) -Copyright (c) 2021 Haydn Paterson (sinclair) +Copyright (c) 2017-2025 Haydn Paterson (sinclair) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/package-lock.json b/package-lock.json index 2a7201ff4..ed66a3f84 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,251 +1,227 @@ { "name": "@sinclair/typebox", - "version": "0.22.0", + "version": "0.34.41", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@sinclair/typebox", - "version": "0.22.0", + "version": "0.34.41", "license": "MIT", "devDependencies": { - "@sinclair/hammer": "^0.15.8", - "@types/chai": "^4.2.22", - "@types/mocha": "^9.0.0", - "@types/node": "^16.11.9", - "ajv": "^8.8.2", + "@arethetypeswrong/cli": "^0.13.2", + "@sinclair/hammer": "^0.18.0", + "@types/mocha": "^9.1.1", + "@types/node": "^22.13.5", + "ajv": "^8.12.0", "ajv-formats": "^2.1.1", - "chai": "^4.3.4", - "mocha": "^9.1.3", - "tsd": "^0.19.0", - "typescript": "^4.5.2" + "mocha": "^11.1.0", + "prettier": "^2.7.1", + "typescript": "^5.9.2" } }, - "node_modules/@babel/code-frame": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.0.tgz", - "integrity": "sha512-IF4EOMEV+bfYwOmNxGzSnjR2EmQod7f1UXOpZM3l4i4o4QNwzjtJAu/HxdjHq0aYBvdqMuQEY1eg0nqW9ZPORA==", + "node_modules/@andrewbranch/untar.js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@andrewbranch/untar.js/-/untar.js-1.0.3.tgz", + "integrity": "sha512-Jh15/qVmrLGhkKJBdXlK1+9tY4lZruYjsgkDFj08ZmDiWVBLJcqkok7Z0/R0In+i1rScBpJlSvrTS2Lm41Pbnw==", + "dev": true + }, + "node_modules/@arethetypeswrong/cli": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/@arethetypeswrong/cli/-/cli-0.13.2.tgz", + "integrity": "sha512-eqRWeFFiI58xwsiUfZSdZsmNCaqqtxmSPP9554ajiCDrB/aNzq5VktVK7dNiT9PamunNeoej4KbDBnkNwVacvg==", "dev": true, "dependencies": { - "@babel/highlight": "^7.16.0" + "@arethetypeswrong/core": "0.13.2", + "chalk": "^4.1.2", + "cli-table3": "^0.6.3", + "commander": "^10.0.1", + "marked": "^9.1.2", + "marked-terminal": "^6.0.0", + "semver": "^7.5.4" + }, + "bin": { + "attw": "dist/index.js" }, "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.15.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", - "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", - "dev": true, - "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@babel/highlight": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.0.tgz", - "integrity": "sha512-t8MH41kUQylBtu2+4IQA3atqevA2lRgqA2wyVB/YiWmsDSuylZZuXOUy9ric30hfzauEFfdsuk/eXTRrGrfd0g==", + "node_modules/@arethetypeswrong/core": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/@arethetypeswrong/core/-/core-0.13.2.tgz", + "integrity": "sha512-1l6ygar+6TH4o1JipWWGCEZlOhAwEShm1yKx+CgIByNjCzufbu6k9DNbDmBjdouusNRhBIOYQe1UHnJig+GtAw==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.15.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" + "@andrewbranch/untar.js": "^1.0.3", + "fflate": "^0.7.4", + "semver": "^7.5.4", + "typescript": "5.3.2", + "validate-npm-package-name": "^5.0.0" }, "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "node_modules/@arethetypeswrong/core/node_modules/typescript": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz", + "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==", "dev": true, - "dependencies": { - "color-convert": "^1.9.0" + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4" + "node": ">=14.17" } }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, + "optional": true, "engines": { - "node": ">=4" + "node": ">=0.1.90" } }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "node_modules/@esbuild/linux-loong64": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.7.tgz", + "integrity": "sha512-IKznSJOsVUuyt7cDzzSZyqBEcZe+7WlBqTVXiF1OXP/4Nm387ToaXZ0fyLwI1iBlI/bzpxVq411QE2/Bt2XWWw==", + "cpu": [ + "loong64" + ], "dev": true, - "dependencies": { - "color-name": "1.1.3" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, "engines": { - "node": ">=0.8.0" + "node": ">=12" } }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, "engines": { - "node": ">=4" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, "dependencies": { - "has-flag": "^3.0.0" + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" }, "engines": { - "node": ">=4" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "ansi-regex": "^6.0.1" }, "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "engines": { - "node": ">= 8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "dev": true, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, + "optional": true, "engines": { - "node": ">= 8" + "node": ">=14" } }, "node_modules/@sinclair/hammer": { - "version": "0.15.8", - "resolved": "https://registry.npmjs.org/@sinclair/hammer/-/hammer-0.15.8.tgz", - "integrity": "sha512-XM6SOQkF8HusX6nCZrlwpU1O+QtjNyhvrF0B6loZReJAuAZJJuTd4/fFDErCrp8uGauG8alYNXcpEOmQRvLARw==", + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@sinclair/hammer/-/hammer-0.18.0.tgz", + "integrity": "sha512-nIrsOWvvCV/SjTSU4qcOsY3mEO8ErN2WBWzbn6mVpqDajy36lG9WbDEfR6Agm3LbN2pdPl1HGjKuiHpbpOTZ2A==", "dev": true, "dependencies": { - "esbuild": "^0.12.24" + "esbuild": "0.15.7" }, "bin": { "hammer": "hammer" } }, - "node_modules/@tsd/typescript": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@tsd/typescript/-/typescript-4.5.2.tgz", - "integrity": "sha512-K778wcPuAsJ9Ch0/FlhQcaIMFEi+TfxNi5NSUbgZd3RucaktYUpR++1Ox2mW2G25oyxWb8gfgg+JUhulRbI6eQ==", + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", "dev": true, - "bin": { - "tsc": "typescript/bin/tsc", - "tsserver": "typescript/bin/tsserver" + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" } }, - "node_modules/@types/chai": { - "version": "4.2.22", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.22.tgz", - "integrity": "sha512-tFfcE+DSTzWAgifkjik9AySNqIyNoYwmR+uecPwwD/XRNfvOjmC/FjCxpiUGDkDVDphPfCUecSQVFw+lN3M3kQ==", + "node_modules/@types/mocha": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.1.1.tgz", + "integrity": "sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw==", "dev": true }, - "node_modules/@types/eslint": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.29.0.tgz", - "integrity": "sha512-VNcvioYDH8/FxaeTKkM4/TiTwt6pBV9E3OfGmvaw8tPl0rrHCJ4Ll15HRT+pMiFAf/MLQvAzC+6RzUMEL9Ceng==", + "node_modules/@types/node": { + "version": "22.13.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.5.tgz", + "integrity": "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg==", "dev": true, "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" + "undici-types": "~6.20.0" } }, - "node_modules/@types/estree": { - "version": "0.0.50", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.50.tgz", - "integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==", - "dev": true - }, - "node_modules/@types/json-schema": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", - "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", - "dev": true - }, - "node_modules/@types/minimist": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz", - "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==", - "dev": true - }, - "node_modules/@types/mocha": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.0.0.tgz", - "integrity": "sha512-scN0hAWyLVAvLR9AyW7HoFF5sJZglyBsbPuHO4fv7JRvfmPBMfp1ozWqOf/e4wwPNxezBZXRfWzMb6iFLgEVRA==", - "dev": true - }, - "node_modules/@types/node": { - "version": "16.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.9.tgz", - "integrity": "sha512-MKmdASMf3LtPzwLyRrFjtFFZ48cMf8jmX5VRYrDQiJa8Ybu5VAmkqBWqKU8fdCwD8ysw4mQ9nrEHvzg6gunR7A==", - "dev": true - }, - "node_modules/@types/normalize-package-data": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", - "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", - "dev": true - }, - "node_modules/@ungap/promise-all-settled": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", - "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", - "dev": true - }, "node_modules/ajv": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.8.2.tgz", - "integrity": "sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw==", + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", "dev": true, "dependencies": { "fast-deep-equal": "^3.1.1", @@ -276,24 +252,24 @@ } }, "node_modules/ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "dev": true, "engines": { "node": ">=6" } }, "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.0.tgz", + "integrity": "sha512-kzRaCqXnpzWs+3z5ABPQiVke+iq0KXkHo8xiWV4RPTi5Yli0l97BEQuhXV1s7+aSU/fu1kUuxgS4MsQ0fRuygw==", "dev": true, "dependencies": { - "type-fest": "^0.21.3" + "type-fest": "^3.0.0" }, "engines": { - "node": ">=8" + "node": ">=14.16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -323,10 +299,16 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/ansicolors": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz", + "integrity": "sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg==", + "dev": true + }, "node_modules/anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, "dependencies": { "normalize-path": "^3.0.0", @@ -342,33 +324,6 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -385,22 +340,21 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -412,59 +366,38 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, - "node_modules/camelcase": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", - "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", + "node_modules/builtins": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz", + "integrity": "sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==", "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "dependencies": { + "semver": "^7.0.0" } }, - "node_modules/camelcase-keys": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", - "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true, - "dependencies": { - "camelcase": "^5.3.1", - "map-obj": "^4.0.0", - "quick-lru": "^4.0.1" - }, "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/camelcase-keys/node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/chai": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.4.tgz", - "integrity": "sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==", + "node_modules/cardinal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/cardinal/-/cardinal-2.1.1.tgz", + "integrity": "sha512-JSr5eOgoEymtYHBjNWyjrMqet9Am2miJhlfKNdqLp6zoeAh0KN5dRAcxlecj5mAJrmQomgiOBj35xHLrFjqBpw==", "dev": true, "dependencies": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^3.0.1", - "get-func-name": "^2.0.0", - "pathval": "^1.1.1", - "type-detect": "^4.0.5" + "ansicolors": "~0.3.2", + "redeyed": "~2.1.0" }, - "engines": { - "node": ">=4" + "bin": { + "cdl": "bin/cdl.js" } }, "node_modules/chalk": { @@ -495,20 +428,26 @@ "node": ">=8" } }, - "node_modules/check-error": { + "node_modules/char-regex": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", "dev": true, "engines": { - "node": "*" + "node": ">=10" } }, "node_modules/chokidar": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", - "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -525,15 +464,50 @@ "fsevents": "~2.3.2" } }, + "node_modules/cli-table3": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.3.tgz", + "integrity": "sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, "dependencies": { "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", + "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "node_modules/color-convert": { @@ -554,19 +528,36 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } }, "node_modules/debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -577,12 +568,6 @@ } } }, - "node_modules/debug/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "node_modules/decamelize": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", @@ -595,69 +580,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/decamelize-keys": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz", - "integrity": "sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=", - "dev": true, - "dependencies": { - "decamelize": "^1.1.0", - "map-obj": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/decamelize-keys/node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/decamelize-keys/node_modules/map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/deep-eql": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", - "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", - "dev": true, - "dependencies": { - "type-detect": "^4.0.0" - }, - "engines": { - "node": ">=0.12" - } - }, "node_modules/diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", "dev": true, "engines": { "node": ">=0.3.1" } }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true }, "node_modules/emoji-regex": { "version": "8.0.0", @@ -665,109 +601,418 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "dependencies": { - "is-arrayish": "^0.2.1" - } + "node_modules/emojilib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/emojilib/-/emojilib-2.4.0.tgz", + "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==", + "dev": true }, "node_modules/esbuild": { - "version": "0.12.29", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.29.tgz", - "integrity": "sha512-w/XuoBCSwepyiZtIRsKsetiLDUVGPVw1E/R3VTFSecIy8UR7Cq3SOtwKHJMFoVqqVG36aGkzh4e8BvpO1Fdc7g==", + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.7.tgz", + "integrity": "sha512-7V8tzllIbAQV1M4QoE52ImKu8hT/NLGlGXkiDsbEU5PS6K8Mn09ZnYoS+dcmHxOS9CRsV4IRAMdT3I67IyUNXw==", "dev": true, "hasInstallScript": true, "bin": { "esbuild": "bin/esbuild" - } - }, - "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/linux-loong64": "0.15.7", + "esbuild-android-64": "0.15.7", + "esbuild-android-arm64": "0.15.7", + "esbuild-darwin-64": "0.15.7", + "esbuild-darwin-arm64": "0.15.7", + "esbuild-freebsd-64": "0.15.7", + "esbuild-freebsd-arm64": "0.15.7", + "esbuild-linux-32": "0.15.7", + "esbuild-linux-64": "0.15.7", + "esbuild-linux-arm": "0.15.7", + "esbuild-linux-arm64": "0.15.7", + "esbuild-linux-mips64le": "0.15.7", + "esbuild-linux-ppc64le": "0.15.7", + "esbuild-linux-riscv64": "0.15.7", + "esbuild-linux-s390x": "0.15.7", + "esbuild-netbsd-64": "0.15.7", + "esbuild-openbsd-64": "0.15.7", + "esbuild-sunos-64": "0.15.7", + "esbuild-windows-32": "0.15.7", + "esbuild-windows-64": "0.15.7", + "esbuild-windows-arm64": "0.15.7" + } + }, + "node_modules/esbuild-android-64": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.7.tgz", + "integrity": "sha512-p7rCvdsldhxQr3YHxptf1Jcd86dlhvc3EQmQJaZzzuAxefO9PvcI0GLOa5nCWem1AJ8iMRu9w0r5TG8pHmbi9w==", + "cpu": [ + "x64" + ], "dev": true, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=6" + "node": ">=12" } }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "node_modules/esbuild-android-arm64": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.7.tgz", + "integrity": "sha512-L775l9ynJT7rVqRM5vo+9w5g2ysbOCfsdLV4CWanTZ1k/9Jb3IYlQ06VCI1edhcosTYJRECQFJa3eAvkx72eyQ==", + "cpu": [ + "arm64" + ], "dev": true, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=12" } }, - "node_modules/eslint-formatter-pretty": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/eslint-formatter-pretty/-/eslint-formatter-pretty-4.1.0.tgz", - "integrity": "sha512-IsUTtGxF1hrH6lMWiSl1WbGaiP01eT6kzywdY1U+zLc0MP+nwEnUiS9UI8IaOTUhTeQJLlCEWIbXINBH4YJbBQ==", + "node_modules/esbuild-darwin-64": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.7.tgz", + "integrity": "sha512-KGPt3r1c9ww009t2xLB6Vk0YyNOXh7hbjZ3EecHoVDxgtbUlYstMPDaReimKe6eOEfyY4hBEEeTvKwPsiH5WZg==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "@types/eslint": "^7.2.13", - "ansi-escapes": "^4.2.1", - "chalk": "^4.1.0", - "eslint-rule-docs": "^1.1.5", - "log-symbols": "^4.0.0", - "plur": "^4.0.0", - "string-width": "^4.2.0", - "supports-hyperlinks": "^2.0.0" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=12" } }, - "node_modules/eslint-rule-docs": { - "version": "1.1.231", - "resolved": "https://registry.npmjs.org/eslint-rule-docs/-/eslint-rule-docs-1.1.231.tgz", - "integrity": "sha512-egHz9A1WG7b8CS0x1P6P/Rj5FqZOjray/VjpJa14tMZalfRKvpE2ONJ3plCM7+PcinmU4tcmbPLv0VtwzSdLVA==", - "dev": true - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "node_modules/esbuild-darwin-arm64": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.7.tgz", + "integrity": "sha512-kBIHvtVqbSGajN88lYMnR3aIleH3ABZLLFLxwL2stiuIGAjGlQW741NxVTpUHQXUmPzxi6POqc9npkXa8AcSZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } }, - "node_modules/fast-glob": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", - "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==", + "node_modules/esbuild-freebsd-64": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.7.tgz", + "integrity": "sha512-hESZB91qDLV5MEwNxzMxPfbjAhOmtfsr9Wnuci7pY6TtEh4UDuevmGmkUIjX/b+e/k4tcNBMf7SRQ2mdNuK/HQ==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=8" + "node": ">=12" } }, - "node_modules/fastq": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", - "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "node_modules/esbuild-freebsd-arm64": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.7.tgz", + "integrity": "sha512-dLFR0ChH5t+b3J8w0fVKGvtwSLWCv7GYT2Y2jFGulF1L5HftQLzVGN+6pi1SivuiVSmTh28FwUhi9PwQicXI6Q==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "reusify": "^1.0.4" + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-32": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.7.tgz", + "integrity": "sha512-v3gT/LsONGUZcjbt2swrMjwxo32NJzk+7sAgtxhGx1+ZmOFaTRXBAi1PPfgpeo/J//Un2jIKm/I+qqeo4caJvg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.7.tgz", + "integrity": "sha512-LxXEfLAKwOVmm1yecpMmWERBshl+Kv5YJ/1KnyAr6HRHFW8cxOEsEfisD3sVl/RvHyW//lhYUVSuy9jGEfIRAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.7.tgz", + "integrity": "sha512-JKgAHtMR5f75wJTeuNQbyznZZa+pjiUHV7sRZp42UNdyXC6TiUYMW/8z8yIBAr2Fpad8hM1royZKQisqPABPvQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.7.tgz", + "integrity": "sha512-P3cfhudpzWDkglutWgXcT2S7Ft7o2e3YDMrP1n0z2dlbUZghUkKCyaWw0zhp4KxEEzt/E7lmrtRu/pGWnwb9vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.7.tgz", + "integrity": "sha512-T7XKuxl0VpeFLCJXub6U+iybiqh0kM/bWOTb4qcPyDDwNVhLUiPcGdG2/0S7F93czUZOKP57YiLV8YQewgLHKw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.7.tgz", + "integrity": "sha512-6mGuC19WpFN7NYbecMIJjeQgvDb5aMuvyk0PDYBJrqAEMkTwg3Z98kEKuCm6THHRnrgsdr7bp4SruSAxEM4eJw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.7.tgz", + "integrity": "sha512-uUJsezbswAYo/X7OU/P+PuL/EI9WzxsEQXDekfwpQ23uGiooxqoLFAPmXPcRAt941vjlY9jtITEEikWMBr+F/g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, + "node_modules/esbuild-linux-s390x": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.7.tgz", + "integrity": "sha512-+tO+xOyTNMc34rXlSxK7aCwJgvQyffqEM5MMdNDEeMU3ss0S6wKvbBOQfgd5jRPblfwJ6b+bKiz0g5nABpY0QQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-netbsd-64": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.7.tgz", + "integrity": "sha512-yVc4Wz+Pu3cP5hzm5kIygNPrjar/v5WCSoRmIjCPWfBVJkZNb5brEGKUlf+0Y759D48BCWa0WHrWXaNy0DULTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.7.tgz", + "integrity": "sha512-GsimbwC4FSR4lN3wf8XmTQ+r8/0YSQo21rWDL0XFFhLHKlzEA4SsT1Tl8bPYu00IU6UWSJ+b3fG/8SB69rcuEQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-sunos-64": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.7.tgz", + "integrity": "sha512-8CDI1aL/ts0mDGbWzjEOGKXnU7p3rDzggHSBtVryQzkSOsjCHRVe0iFYUuhczlxU1R3LN/E7HgUO4NXzGGP/Ag==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-32": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.7.tgz", + "integrity": "sha512-cOnKXUEPS8EGCzRSFa1x6NQjGhGsFlVgjhqGEbLTPsA7x4RRYiy2RKoArNUU4iR2vHmzqS5Gr84MEumO/wxYKA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-64": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.7.tgz", + "integrity": "sha512-7MI08Ec2sTIDv+zH6StNBKO+2hGUYIT42GmFyW6MBBWWtJhTcQLinKS6ldIN1d52MXIbiJ6nXyCJ+LpL4jBm3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.7.tgz", + "integrity": "sha512-R06nmqBlWjKHddhRJYlqDd3Fabx9LFdKcjoOy08YLimwmsswlFBJV4rXzZCxz/b7ZJXvrZgj8DDv1ewE9+StMw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fflate": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz", + "integrity": "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==", + "dev": true + }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -801,11 +1046,21 @@ "flat": "cli.js" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, "node_modules/fsevents": { "version": "2.3.2", @@ -821,12 +1076,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -836,30 +1085,21 @@ "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/glob": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", - "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, - "engines": { - "node": "*" + "bin": { + "glob": "dist/esm/bin.mjs" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -877,54 +1117,19 @@ "node": ">= 6" } }, - "node_modules/globby": { - "version": "11.0.4", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz", - "integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==", + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.1.1", - "ignore": "^5.1.4", - "merge2": "^1.3.0", - "slash": "^3.0.0" + "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=10" + "node": ">=16 || 14 >=14.17" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/growl": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", - "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", - "dev": true, - "engines": { - "node": ">=4.x" - } - }, - "node_modules/hard-rejection": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", - "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/has-flag": { @@ -945,67 +1150,6 @@ "he": "bin/he" } }, - "node_modules/hosted-git-info": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.0.2.tgz", - "integrity": "sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/ignore": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.9.tgz", - "integrity": "sha512-2zeMQpbKz5dhZ9IwL0gbxSW5w0NK/MSAMtNuhgIHEPmaU3vPdKPL0UdvUCXs5SS4JAwsBxysK5sFMW8ocFiVjQ==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "node_modules/irregular-plurals": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-3.3.0.tgz", - "integrity": "sha512-MVBLKUTangM3EfRPFROhmWQQKRDsrgI83J8GS3jXy+OwYqiR2/aoWndYQ5416jLE3uaGgLH7ncme3X9y09gZ3g==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -1018,22 +1162,10 @@ "node": ">=8" } }, - "node_modules/is-core-module": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", - "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", - "dev": true, - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, "engines": { "node": ">=0.10.0" @@ -1049,9 +1181,9 @@ } }, "node_modules/is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "dependencies": { "is-extglob": "^2.1.1" @@ -1093,14 +1225,23 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } }, "node_modules/js-yaml": { "version": "4.1.0", @@ -1114,33 +1255,12 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/lines-and-columns": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", - "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", - "dev": true - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -1184,525 +1304,255 @@ "node": ">=10" } }, - "node_modules/map-obj": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", - "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/meow": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", - "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==", - "dev": true, - "dependencies": { - "@types/minimist": "^1.2.0", - "camelcase-keys": "^6.2.2", - "decamelize": "^1.2.0", - "decamelize-keys": "^1.1.0", - "hard-rejection": "^2.1.0", - "minimist-options": "4.1.0", - "normalize-package-data": "^3.0.0", - "read-pkg-up": "^7.0.1", - "redent": "^3.0.0", - "trim-newlines": "^3.0.0", - "type-fest": "^0.18.0", - "yargs-parser": "^20.2.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/meow/node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/meow/node_modules/type-fest": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", - "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "node_modules/marked": { + "version": "9.1.6", + "resolved": "https://registry.npmjs.org/marked/-/marked-9.1.6.tgz", + "integrity": "sha512-jcByLnIFkd5gSXZmjNvS1TlmRhCXZjIzHYlaGkPlLIekG55JDR2Z4va9tZwCiP+/RDERiNhMOFu01xd6O5ct1Q==", "dev": true, - "dependencies": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist-options": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", - "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", - "dev": true, - "dependencies": { - "arrify": "^1.0.1", - "is-plain-obj": "^1.1.0", - "kind-of": "^6.0.3" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/minimist-options/node_modules/is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/mocha": { - "version": "9.1.3", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.1.3.tgz", - "integrity": "sha512-Xcpl9FqXOAYqI3j79pEtHBBnQgVXIhpULjGQa7DVb0Po+VzmSIK9kanAiWLHoRR/dbZ2qpdPshuXr8l1VaHCzw==", - "dev": true, - "dependencies": { - "@ungap/promise-all-settled": "1.1.2", - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.2", - "debug": "4.3.2", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.1.7", - "growl": "1.10.5", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "3.0.4", - "ms": "2.1.3", - "nanoid": "3.1.25", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "which": "2.0.2", - "workerpool": "6.1.5", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" - }, "bin": { - "_mocha": "bin/_mocha", - "mocha": "bin/mocha" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mochajs" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, - "node_modules/nanoid": { - "version": "3.1.25", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.25.tgz", - "integrity": "sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q==", - "dev": true, - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/normalize-package-data": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", - "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", - "dev": true, - "dependencies": { - "hosted-git-info": "^4.0.1", - "is-core-module": "^2.5.0", - "semver": "^7.3.4", - "validate-npm-package-license": "^3.0.1" + "marked": "bin/marked.js" }, "engines": { - "node": ">=10" + "node": ">= 16" } }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "node_modules/marked-terminal": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-6.1.0.tgz", + "integrity": "sha512-QaCSF6NV82oo6K0szEnmc65ooDeW0T/Adcyf0fcW+Hto2GT1VADFg8dn1zaeHqzj65fqDH1hMNChGNRaC/lbkA==", "dev": true, "dependencies": { - "wrappy": "1" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" + "ansi-escapes": "^6.2.0", + "cardinal": "^2.1.1", + "chalk": "^5.3.0", + "cli-table3": "^0.6.3", + "node-emoji": "^2.1.0", + "supports-hyperlinks": "^3.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true, "engines": { - "node": "*" + "node": ">=16.0.0" + }, + "peerDependencies": { + "marked": ">=1 <11" } }, - "node_modules/picomatch": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", - "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "node_modules/marked-terminal/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", "dev": true, "engines": { - "node": ">=8.6" + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/plur": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/plur/-/plur-4.0.0.tgz", - "integrity": "sha512-4UGewrYgqDFw9vV6zNV+ADmPAUAfJPKtGvb/VdpQAx25X5f3xXdGdyOEVFwkl8Hl/tl7+xbeHqSEM+D5/TirUg==", + "node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, "dependencies": { - "irregular-plurals": "^3.2.0" + "brace-expansion": "^2.0.1" }, "engines": { "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true, "engines": { - "node": ">=6" + "node": ">=16 || 14 >=14.17" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] + "node_modules/mocha": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.1.0.tgz", + "integrity": "sha512-8uJR5RTC2NgpY3GrYcgpZrsEd9zKbPDpob1RezyR2upGHRQtHWofmzTMzTMSV6dru3tj5Ukt0+Vnq1qhFEEwAg==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } }, - "node_modules/quick-lru": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", - "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/node-emoji": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.1.3.tgz", + "integrity": "sha512-E2WEOVsgs7O16zsURJ/eH8BqhF029wGpEOnv7Urwdo2wmQanOACwJQh0devF9D9RhoZru0+9JXIS0dBXIAz+lA==", "dev": true, + "dependencies": { + "@sindresorhus/is": "^4.6.0", + "char-regex": "^1.0.2", + "emojilib": "^2.4.0", + "skin-tone": "^2.0.0" + }, "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, - "dependencies": { - "safe-buffer": "^5.1.0" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "dependencies": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" + "yocto-queue": "^0.1.0" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/read-pkg-up": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "dependencies": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" + "p-limit": "^3.0.2" }, "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/read-pkg-up/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, "engines": { "node": ">=8" } }, - "node_modules/read-pkg-up/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, "engines": { "node": ">=8" } }, - "node_modules/read-pkg-up/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, "dependencies": { - "p-try": "^2.0.0" + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { - "node": ">=6" + "node": ">=16 || 14 >=14.18" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/read-pkg-up/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true }, - "node_modules/read-pkg-up/node_modules/type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, "engines": { - "node": ">=8" + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/read-pkg/node_modules/hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true - }, - "node_modules/read-pkg/node_modules/normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "dev": true, - "dependencies": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/read-pkg/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", "dev": true, - "bin": { - "semver": "bin/semver" + "engines": { + "node": ">=6" } }, - "node_modules/read-pkg/node_modules/type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dev": true, - "engines": { - "node": ">=8" + "dependencies": { + "safe-buffer": "^5.1.0" } }, "node_modules/readdirp": { @@ -1717,23 +1567,19 @@ "node": ">=8.10.0" } }, - "node_modules/redent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "node_modules/redeyed": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/redeyed/-/redeyed-2.1.1.tgz", + "integrity": "sha512-FNpGGo1DycYAdnrKFxCMmKYgo/mILAqtRYbkdQD8Ep/Hk2PQ5+aEAEx+IU713RTDmuBaH0c8P5ZozurNu5ObRQ==", "dev": true, "dependencies": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - }, - "engines": { - "node": ">=8" + "esprima": "~4.0.0" } }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true, "engines": { "node": ">=0.10.0" @@ -1748,52 +1594,6 @@ "node": ">=0.10.0" } }, - "node_modules/resolve": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", - "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", - "dev": true, - "dependencies": { - "is-core-module": "^2.2.0", - "path-parse": "^1.0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1815,9 +1615,9 @@ ] }, "node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -1830,55 +1630,59 @@ } }, "node_modules/serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "dependencies": { "randombytes": "^2.1.0" } }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, "engines": { "node": ">=8" } }, - "node_modules/spdx-correct": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", - "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" + "engines": { + "node": ">=8" } }, - "node_modules/spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", - "dev": true + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, - "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "node_modules/skin-tone": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/skin-tone/-/skin-tone-2.0.0.tgz", + "integrity": "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==", "dev": true, "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" + "unicode-emoji-modifier-base": "^1.0.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/spdx-license-ids": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz", - "integrity": "sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g==", - "dev": true - }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -1893,6 +1697,21 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -1905,13 +1724,14 @@ "node": ">=8" } }, - "node_modules/strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "dependencies": { - "min-indent": "^1.0.0" + "ansi-regex": "^5.0.1" }, "engines": { "node": ">=8" @@ -1945,16 +1765,16 @@ } }, "node_modules/supports-hyperlinks": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz", - "integrity": "sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.0.0.tgz", + "integrity": "sha512-QBDPHyPQDRTy9ku4URNGY5Lah8PAaXs6tAAwp55sL5WCsSW7GIfdf6W5ixfziW+t7wh3GVvHyHHyQ1ESsoRvaA==", "dev": true, "dependencies": { "has-flag": "^4.0.0", "supports-color": "^7.0.0" }, "engines": { - "node": ">=8" + "node": ">=14.18" } }, "node_modules/supports-hyperlinks/node_modules/supports-color": { @@ -1981,67 +1801,44 @@ "node": ">=8.0" } }, - "node_modules/trim-newlines": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", - "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/tsd": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/tsd/-/tsd-0.19.0.tgz", - "integrity": "sha512-DlYCjjRVspfSAC+9X1NUDqPj0PtsVjUSyC4/OJpCGnY2Mtg0ddc+opckmnLV3RhUBpVuAAOrz661v+QSJwINkQ==", - "dev": true, - "dependencies": { - "@tsd/typescript": "~4.5.2", - "eslint-formatter-pretty": "^4.1.0", - "globby": "^11.0.1", - "meow": "^9.0.0", - "path-exists": "^4.0.0", - "read-pkg-up": "^7.0.0" - }, - "bin": { - "tsd": "dist/cli.js" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", + "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", "dev": true, "engines": { - "node": ">=10" + "node": ">=14.16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/typescript": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz", - "integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==", + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true + }, + "node_modules/unicode-emoji-modifier-base": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz", + "integrity": "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==", + "dev": true, + "engines": { + "node": ">=4" } }, "node_modules/uri-js": { @@ -2053,14 +1850,16 @@ "punycode": "^2.1.0" } }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "node_modules/validate-npm-package-name": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.0.tgz", + "integrity": "sha512-YuKoXDAhBYxY7SfOKxHBDoSyENFeW5VvIIQp2TGQuit8gpK6MnWaQelBKxso72DoxTZfZdcP3W90LqpSkgPzLQ==", "dev": true, "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" + "builtins": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/which": { @@ -2078,13 +1877,31 @@ "node": ">= 8" } }, - "node_modules/workerpool": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.1.5.tgz", - "integrity": "sha512-XdKkCK0Zqc6w3iTxLckiuJ81tiD/o5rBE/m+nXpRCB+/Sq4DqkfXZ/x0jW02DG1tGsfUGXbTJyZDP+eu67haSw==", - "dev": true - }, - "node_modules/wrap-ansi": { + "node_modules/workerpool": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "dev": true + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", @@ -2101,12 +1918,68 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -2123,30 +1996,30 @@ "dev": true }, "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, "dependencies": { - "cliui": "^7.0.2", + "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "string-width": "^4.2.0", + "string-width": "^4.2.3", "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" + "yargs-parser": "^21.1.1" }, "engines": { - "node": ">=10" + "node": ">=12" } }, "node_modules/yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, "engines": { - "node": ">=10" + "node": ">=12" } }, "node_modules/yargs-unparser": { @@ -2178,193 +2051,151 @@ } }, "dependencies": { - "@babel/code-frame": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.0.tgz", - "integrity": "sha512-IF4EOMEV+bfYwOmNxGzSnjR2EmQod7f1UXOpZM3l4i4o4QNwzjtJAu/HxdjHq0aYBvdqMuQEY1eg0nqW9ZPORA==", + "@andrewbranch/untar.js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@andrewbranch/untar.js/-/untar.js-1.0.3.tgz", + "integrity": "sha512-Jh15/qVmrLGhkKJBdXlK1+9tY4lZruYjsgkDFj08ZmDiWVBLJcqkok7Z0/R0In+i1rScBpJlSvrTS2Lm41Pbnw==", + "dev": true + }, + "@arethetypeswrong/cli": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/@arethetypeswrong/cli/-/cli-0.13.2.tgz", + "integrity": "sha512-eqRWeFFiI58xwsiUfZSdZsmNCaqqtxmSPP9554ajiCDrB/aNzq5VktVK7dNiT9PamunNeoej4KbDBnkNwVacvg==", "dev": true, "requires": { - "@babel/highlight": "^7.16.0" + "@arethetypeswrong/core": "0.13.2", + "chalk": "^4.1.2", + "cli-table3": "^0.6.3", + "commander": "^10.0.1", + "marked": "^9.1.2", + "marked-terminal": "^6.0.0", + "semver": "^7.5.4" } }, - "@babel/helper-validator-identifier": { - "version": "7.15.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", - "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", - "dev": true + "@arethetypeswrong/core": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/@arethetypeswrong/core/-/core-0.13.2.tgz", + "integrity": "sha512-1l6ygar+6TH4o1JipWWGCEZlOhAwEShm1yKx+CgIByNjCzufbu6k9DNbDmBjdouusNRhBIOYQe1UHnJig+GtAw==", + "dev": true, + "requires": { + "@andrewbranch/untar.js": "^1.0.3", + "fflate": "^0.7.4", + "semver": "^7.5.4", + "typescript": "5.3.2", + "validate-npm-package-name": "^5.0.0" + }, + "dependencies": { + "typescript": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz", + "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==", + "dev": true + } + } + }, + "@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.7.tgz", + "integrity": "sha512-IKznSJOsVUuyt7cDzzSZyqBEcZe+7WlBqTVXiF1OXP/4Nm387ToaXZ0fyLwI1iBlI/bzpxVq411QE2/Bt2XWWw==", + "dev": true, + "optional": true }, - "@babel/highlight": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.0.tgz", - "integrity": "sha512-t8MH41kUQylBtu2+4IQA3atqevA2lRgqA2wyVB/YiWmsDSuylZZuXOUy9ric30hfzauEFfdsuk/eXTRrGrfd0g==", + "@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.15.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" }, "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } + "ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, "requires": { - "color-name": "1.1.3" + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" } }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, "requires": { - "has-flag": "^3.0.0" + "ansi-regex": "^6.0.1" } } } }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } + "optional": true }, "@sinclair/hammer": { - "version": "0.15.8", - "resolved": "https://registry.npmjs.org/@sinclair/hammer/-/hammer-0.15.8.tgz", - "integrity": "sha512-XM6SOQkF8HusX6nCZrlwpU1O+QtjNyhvrF0B6loZReJAuAZJJuTd4/fFDErCrp8uGauG8alYNXcpEOmQRvLARw==", + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@sinclair/hammer/-/hammer-0.18.0.tgz", + "integrity": "sha512-nIrsOWvvCV/SjTSU4qcOsY3mEO8ErN2WBWzbn6mVpqDajy36lG9WbDEfR6Agm3LbN2pdPl1HGjKuiHpbpOTZ2A==", "dev": true, "requires": { - "esbuild": "^0.12.24" + "esbuild": "0.15.7" } }, - "@tsd/typescript": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@tsd/typescript/-/typescript-4.5.2.tgz", - "integrity": "sha512-K778wcPuAsJ9Ch0/FlhQcaIMFEi+TfxNi5NSUbgZd3RucaktYUpR++1Ox2mW2G25oyxWb8gfgg+JUhulRbI6eQ==", + "@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", "dev": true }, - "@types/chai": { - "version": "4.2.22", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.22.tgz", - "integrity": "sha512-tFfcE+DSTzWAgifkjik9AySNqIyNoYwmR+uecPwwD/XRNfvOjmC/FjCxpiUGDkDVDphPfCUecSQVFw+lN3M3kQ==", + "@types/mocha": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.1.1.tgz", + "integrity": "sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw==", "dev": true }, - "@types/eslint": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.29.0.tgz", - "integrity": "sha512-VNcvioYDH8/FxaeTKkM4/TiTwt6pBV9E3OfGmvaw8tPl0rrHCJ4Ll15HRT+pMiFAf/MLQvAzC+6RzUMEL9Ceng==", + "@types/node": { + "version": "22.13.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.5.tgz", + "integrity": "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg==", "dev": true, "requires": { - "@types/estree": "*", - "@types/json-schema": "*" + "undici-types": "~6.20.0" } }, - "@types/estree": { - "version": "0.0.50", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.50.tgz", - "integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==", - "dev": true - }, - "@types/json-schema": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", - "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", - "dev": true - }, - "@types/minimist": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz", - "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==", - "dev": true - }, - "@types/mocha": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.0.0.tgz", - "integrity": "sha512-scN0hAWyLVAvLR9AyW7HoFF5sJZglyBsbPuHO4fv7JRvfmPBMfp1ozWqOf/e4wwPNxezBZXRfWzMb6iFLgEVRA==", - "dev": true - }, - "@types/node": { - "version": "16.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.9.tgz", - "integrity": "sha512-MKmdASMf3LtPzwLyRrFjtFFZ48cMf8jmX5VRYrDQiJa8Ybu5VAmkqBWqKU8fdCwD8ysw4mQ9nrEHvzg6gunR7A==", - "dev": true - }, - "@types/normalize-package-data": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", - "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", - "dev": true - }, - "@ungap/promise-all-settled": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", - "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", - "dev": true - }, "ajv": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.8.2.tgz", - "integrity": "sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw==", + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", "dev": true, "requires": { "fast-deep-equal": "^3.1.1", @@ -2383,18 +2214,18 @@ } }, "ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "dev": true }, "ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.0.tgz", + "integrity": "sha512-kzRaCqXnpzWs+3z5ABPQiVke+iq0KXkHo8xiWV4RPTi5Yli0l97BEQuhXV1s7+aSU/fu1kUuxgS4MsQ0fRuygw==", "dev": true, "requires": { - "type-fest": "^0.21.3" + "type-fest": "^3.0.0" } }, "ansi-regex": { @@ -2412,10 +2243,16 @@ "color-convert": "^2.0.1" } }, + "ansicolors": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz", + "integrity": "sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg==", + "dev": true + }, "anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, "requires": { "normalize-path": "^3.0.0", @@ -2428,24 +2265,6 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, - "array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true - }, - "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", - "dev": true - }, - "assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true - }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2459,22 +2278,21 @@ "dev": true }, "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "requires": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" } }, "browser-stdout": { @@ -2483,43 +2301,29 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, - "camelcase": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", - "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", - "dev": true - }, - "camelcase-keys": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", - "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", + "builtins": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz", + "integrity": "sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==", "dev": true, "requires": { - "camelcase": "^5.3.1", - "map-obj": "^4.0.0", - "quick-lru": "^4.0.1" - }, - "dependencies": { - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - } + "semver": "^7.0.0" } }, - "chai": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.4.tgz", - "integrity": "sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==", + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true + }, + "cardinal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/cardinal/-/cardinal-2.1.1.tgz", + "integrity": "sha512-JSr5eOgoEymtYHBjNWyjrMqet9Am2miJhlfKNdqLp6zoeAh0KN5dRAcxlecj5mAJrmQomgiOBj35xHLrFjqBpw==", "dev": true, "requires": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^3.0.1", - "get-func-name": "^2.0.0", - "pathval": "^1.1.1", - "type-detect": "^4.0.5" + "ansicolors": "~0.3.2", + "redeyed": "~2.1.0" } }, "chalk": { @@ -2543,16 +2347,16 @@ } } }, - "check-error": { + "char-regex": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", "dev": true }, "chokidar": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", - "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", "dev": true, "requires": { "anymatch": "~3.1.2", @@ -2565,15 +2369,38 @@ "readdirp": "~3.6.0" } }, + "cli-table3": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.3.tgz", + "integrity": "sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==", + "dev": true, + "requires": { + "@colors/colors": "1.5.0", + "string-width": "^4.2.0" + } + }, "cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, "requires": { "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", + "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" + }, + "dependencies": { + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + } } }, "color-convert": { @@ -2591,27 +2418,30 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", "dev": true }, + "cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, "requires": { - "ms": "2.1.2" - }, - "dependencies": { - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } + "ms": "^2.1.3" } }, "decamelize": { @@ -2620,53 +2450,17 @@ "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", "dev": true }, - "decamelize-keys": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz", - "integrity": "sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=", - "dev": true, - "requires": { - "decamelize": "^1.1.0", - "map-obj": "^1.0.0" - }, - "dependencies": { - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true - }, - "map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", - "dev": true - } - } - }, - "deep-eql": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", - "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", - "dev": true, - "requires": { - "type-detect": "^4.0.0" - } - }, "diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", "dev": true }, - "dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } + "eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true }, "emoji-regex": { "version": "8.0.0", @@ -2674,25 +2468,185 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "requires": { - "is-arrayish": "^0.2.1" - } + "emojilib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/emojilib/-/emojilib-2.4.0.tgz", + "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==", + "dev": true }, "esbuild": { - "version": "0.12.29", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.29.tgz", - "integrity": "sha512-w/XuoBCSwepyiZtIRsKsetiLDUVGPVw1E/R3VTFSecIy8UR7Cq3SOtwKHJMFoVqqVG36aGkzh4e8BvpO1Fdc7g==", - "dev": true + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.7.tgz", + "integrity": "sha512-7V8tzllIbAQV1M4QoE52ImKu8hT/NLGlGXkiDsbEU5PS6K8Mn09ZnYoS+dcmHxOS9CRsV4IRAMdT3I67IyUNXw==", + "dev": true, + "requires": { + "@esbuild/linux-loong64": "0.15.7", + "esbuild-android-64": "0.15.7", + "esbuild-android-arm64": "0.15.7", + "esbuild-darwin-64": "0.15.7", + "esbuild-darwin-arm64": "0.15.7", + "esbuild-freebsd-64": "0.15.7", + "esbuild-freebsd-arm64": "0.15.7", + "esbuild-linux-32": "0.15.7", + "esbuild-linux-64": "0.15.7", + "esbuild-linux-arm": "0.15.7", + "esbuild-linux-arm64": "0.15.7", + "esbuild-linux-mips64le": "0.15.7", + "esbuild-linux-ppc64le": "0.15.7", + "esbuild-linux-riscv64": "0.15.7", + "esbuild-linux-s390x": "0.15.7", + "esbuild-netbsd-64": "0.15.7", + "esbuild-openbsd-64": "0.15.7", + "esbuild-sunos-64": "0.15.7", + "esbuild-windows-32": "0.15.7", + "esbuild-windows-64": "0.15.7", + "esbuild-windows-arm64": "0.15.7" + } + }, + "esbuild-android-64": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.7.tgz", + "integrity": "sha512-p7rCvdsldhxQr3YHxptf1Jcd86dlhvc3EQmQJaZzzuAxefO9PvcI0GLOa5nCWem1AJ8iMRu9w0r5TG8pHmbi9w==", + "dev": true, + "optional": true + }, + "esbuild-android-arm64": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.7.tgz", + "integrity": "sha512-L775l9ynJT7rVqRM5vo+9w5g2ysbOCfsdLV4CWanTZ1k/9Jb3IYlQ06VCI1edhcosTYJRECQFJa3eAvkx72eyQ==", + "dev": true, + "optional": true + }, + "esbuild-darwin-64": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.7.tgz", + "integrity": "sha512-KGPt3r1c9ww009t2xLB6Vk0YyNOXh7hbjZ3EecHoVDxgtbUlYstMPDaReimKe6eOEfyY4hBEEeTvKwPsiH5WZg==", + "dev": true, + "optional": true + }, + "esbuild-darwin-arm64": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.7.tgz", + "integrity": "sha512-kBIHvtVqbSGajN88lYMnR3aIleH3ABZLLFLxwL2stiuIGAjGlQW741NxVTpUHQXUmPzxi6POqc9npkXa8AcSZQ==", + "dev": true, + "optional": true + }, + "esbuild-freebsd-64": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.7.tgz", + "integrity": "sha512-hESZB91qDLV5MEwNxzMxPfbjAhOmtfsr9Wnuci7pY6TtEh4UDuevmGmkUIjX/b+e/k4tcNBMf7SRQ2mdNuK/HQ==", + "dev": true, + "optional": true + }, + "esbuild-freebsd-arm64": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.7.tgz", + "integrity": "sha512-dLFR0ChH5t+b3J8w0fVKGvtwSLWCv7GYT2Y2jFGulF1L5HftQLzVGN+6pi1SivuiVSmTh28FwUhi9PwQicXI6Q==", + "dev": true, + "optional": true + }, + "esbuild-linux-32": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.7.tgz", + "integrity": "sha512-v3gT/LsONGUZcjbt2swrMjwxo32NJzk+7sAgtxhGx1+ZmOFaTRXBAi1PPfgpeo/J//Un2jIKm/I+qqeo4caJvg==", + "dev": true, + "optional": true + }, + "esbuild-linux-64": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.7.tgz", + "integrity": "sha512-LxXEfLAKwOVmm1yecpMmWERBshl+Kv5YJ/1KnyAr6HRHFW8cxOEsEfisD3sVl/RvHyW//lhYUVSuy9jGEfIRAQ==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.7.tgz", + "integrity": "sha512-JKgAHtMR5f75wJTeuNQbyznZZa+pjiUHV7sRZp42UNdyXC6TiUYMW/8z8yIBAr2Fpad8hM1royZKQisqPABPvQ==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm64": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.7.tgz", + "integrity": "sha512-P3cfhudpzWDkglutWgXcT2S7Ft7o2e3YDMrP1n0z2dlbUZghUkKCyaWw0zhp4KxEEzt/E7lmrtRu/pGWnwb9vw==", + "dev": true, + "optional": true + }, + "esbuild-linux-mips64le": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.7.tgz", + "integrity": "sha512-T7XKuxl0VpeFLCJXub6U+iybiqh0kM/bWOTb4qcPyDDwNVhLUiPcGdG2/0S7F93czUZOKP57YiLV8YQewgLHKw==", + "dev": true, + "optional": true + }, + "esbuild-linux-ppc64le": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.7.tgz", + "integrity": "sha512-6mGuC19WpFN7NYbecMIJjeQgvDb5aMuvyk0PDYBJrqAEMkTwg3Z98kEKuCm6THHRnrgsdr7bp4SruSAxEM4eJw==", + "dev": true, + "optional": true + }, + "esbuild-linux-riscv64": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.7.tgz", + "integrity": "sha512-uUJsezbswAYo/X7OU/P+PuL/EI9WzxsEQXDekfwpQ23uGiooxqoLFAPmXPcRAt941vjlY9jtITEEikWMBr+F/g==", + "dev": true, + "optional": true + }, + "esbuild-linux-s390x": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.7.tgz", + "integrity": "sha512-+tO+xOyTNMc34rXlSxK7aCwJgvQyffqEM5MMdNDEeMU3ss0S6wKvbBOQfgd5jRPblfwJ6b+bKiz0g5nABpY0QQ==", + "dev": true, + "optional": true + }, + "esbuild-netbsd-64": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.7.tgz", + "integrity": "sha512-yVc4Wz+Pu3cP5hzm5kIygNPrjar/v5WCSoRmIjCPWfBVJkZNb5brEGKUlf+0Y759D48BCWa0WHrWXaNy0DULTQ==", + "dev": true, + "optional": true + }, + "esbuild-openbsd-64": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.7.tgz", + "integrity": "sha512-GsimbwC4FSR4lN3wf8XmTQ+r8/0YSQo21rWDL0XFFhLHKlzEA4SsT1Tl8bPYu00IU6UWSJ+b3fG/8SB69rcuEQ==", + "dev": true, + "optional": true + }, + "esbuild-sunos-64": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.7.tgz", + "integrity": "sha512-8CDI1aL/ts0mDGbWzjEOGKXnU7p3rDzggHSBtVryQzkSOsjCHRVe0iFYUuhczlxU1R3LN/E7HgUO4NXzGGP/Ag==", + "dev": true, + "optional": true + }, + "esbuild-windows-32": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.7.tgz", + "integrity": "sha512-cOnKXUEPS8EGCzRSFa1x6NQjGhGsFlVgjhqGEbLTPsA7x4RRYiy2RKoArNUU4iR2vHmzqS5Gr84MEumO/wxYKA==", + "dev": true, + "optional": true + }, + "esbuild-windows-64": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.7.tgz", + "integrity": "sha512-7MI08Ec2sTIDv+zH6StNBKO+2hGUYIT42GmFyW6MBBWWtJhTcQLinKS6ldIN1d52MXIbiJ6nXyCJ+LpL4jBm3Q==", + "dev": true, + "optional": true + }, + "esbuild-windows-arm64": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.7.tgz", + "integrity": "sha512-R06nmqBlWjKHddhRJYlqDd3Fabx9LFdKcjoOy08YLimwmsswlFBJV4rXzZCxz/b7ZJXvrZgj8DDv1ewE9+StMw==", + "dev": true, + "optional": true }, "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true }, "escape-string-regexp": { @@ -2701,26 +2655,10 @@ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true }, - "eslint-formatter-pretty": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/eslint-formatter-pretty/-/eslint-formatter-pretty-4.1.0.tgz", - "integrity": "sha512-IsUTtGxF1hrH6lMWiSl1WbGaiP01eT6kzywdY1U+zLc0MP+nwEnUiS9UI8IaOTUhTeQJLlCEWIbXINBH4YJbBQ==", - "dev": true, - "requires": { - "@types/eslint": "^7.2.13", - "ansi-escapes": "^4.2.1", - "chalk": "^4.1.0", - "eslint-rule-docs": "^1.1.5", - "log-symbols": "^4.0.0", - "plur": "^4.0.0", - "string-width": "^4.2.0", - "supports-hyperlinks": "^2.0.0" - } - }, - "eslint-rule-docs": { - "version": "1.1.231", - "resolved": "https://registry.npmjs.org/eslint-rule-docs/-/eslint-rule-docs-1.1.231.tgz", - "integrity": "sha512-egHz9A1WG7b8CS0x1P6P/Rj5FqZOjray/VjpJa14tMZalfRKvpE2ONJ3plCM7+PcinmU4tcmbPLv0VtwzSdLVA==", + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true }, "fast-deep-equal": { @@ -2729,32 +2667,16 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, - "fast-glob": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", - "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - } - }, - "fastq": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", - "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, + "fflate": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz", + "integrity": "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==", + "dev": true + }, "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "requires": { "to-regex-range": "^5.0.1" @@ -2776,11 +2698,15 @@ "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", "dev": true }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true + "foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + } }, "fsevents": { "version": "2.3.2", @@ -2789,36 +2715,35 @@ "dev": true, "optional": true }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, "get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true }, - "get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", - "dev": true - }, "glob": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", - "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "dependencies": { + "minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } } }, "glob-parent": { @@ -2830,41 +2755,6 @@ "is-glob": "^4.0.1" } }, - "globby": { - "version": "11.0.4", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz", - "integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.1.1", - "ignore": "^5.1.4", - "merge2": "^1.3.0", - "slash": "^3.0.0" - } - }, - "growl": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", - "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", - "dev": true - }, - "hard-rejection": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", - "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", - "dev": true - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1" - } - }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2877,55 +2767,6 @@ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true }, - "hosted-git-info": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.0.2.tgz", - "integrity": "sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "ignore": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.9.tgz", - "integrity": "sha512-2zeMQpbKz5dhZ9IwL0gbxSW5w0NK/MSAMtNuhgIHEPmaU3vPdKPL0UdvUCXs5SS4JAwsBxysK5sFMW8ocFiVjQ==", - "dev": true - }, - "indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "irregular-plurals": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-3.3.0.tgz", - "integrity": "sha512-MVBLKUTangM3EfRPFROhmWQQKRDsrgI83J8GS3jXy+OwYqiR2/aoWndYQ5416jLE3uaGgLH7ncme3X9y09gZ3g==", - "dev": true - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - }, "is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -2935,19 +2776,10 @@ "binary-extensions": "^2.0.0" } }, - "is-core-module": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", - "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true }, "is-fullwidth-code-point": { @@ -2957,9 +2789,9 @@ "dev": true }, "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "requires": { "is-extglob": "^2.1.1" @@ -2986,14 +2818,18 @@ "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "requires": { + "@isaacs/cliui": "^8.0.2", + "@pkgjs/parseargs": "^0.11.0" + } }, "js-yaml": { "version": "4.1.0", @@ -3004,30 +2840,12 @@ "argparse": "^2.0.1" } }, - "json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, "json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true }, - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - }, - "lines-and-columns": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", - "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", - "dev": true - }, "locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -3056,126 +2874,75 @@ "yallist": "^4.0.0" } }, - "map-obj": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", - "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "marked": { + "version": "9.1.6", + "resolved": "https://registry.npmjs.org/marked/-/marked-9.1.6.tgz", + "integrity": "sha512-jcByLnIFkd5gSXZmjNvS1TlmRhCXZjIzHYlaGkPlLIekG55JDR2Z4va9tZwCiP+/RDERiNhMOFu01xd6O5ct1Q==", "dev": true }, - "meow": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", - "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==", + "marked-terminal": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-6.1.0.tgz", + "integrity": "sha512-QaCSF6NV82oo6K0szEnmc65ooDeW0T/Adcyf0fcW+Hto2GT1VADFg8dn1zaeHqzj65fqDH1hMNChGNRaC/lbkA==", "dev": true, "requires": { - "@types/minimist": "^1.2.0", - "camelcase-keys": "^6.2.2", - "decamelize": "^1.2.0", - "decamelize-keys": "^1.1.0", - "hard-rejection": "^2.1.0", - "minimist-options": "4.1.0", - "normalize-package-data": "^3.0.0", - "read-pkg-up": "^7.0.1", - "redent": "^3.0.0", - "trim-newlines": "^3.0.0", - "type-fest": "^0.18.0", - "yargs-parser": "^20.2.3" - }, - "dependencies": { - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true - }, - "type-fest": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", - "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "ansi-escapes": "^6.2.0", + "cardinal": "^2.1.1", + "chalk": "^5.3.0", + "cli-table3": "^0.6.3", + "node-emoji": "^2.1.0", + "supports-hyperlinks": "^3.0.0" + }, + "dependencies": { + "chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", "dev": true } } }, - "merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true - }, - "micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" - } - }, - "min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "dev": true - }, "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, "requires": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" } }, - "minimist-options": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", - "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", - "dev": true, - "requires": { - "arrify": "^1.0.1", - "is-plain-obj": "^1.1.0", - "kind-of": "^6.0.3" - }, - "dependencies": { - "is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", - "dev": true - } - } + "minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true }, "mocha": { - "version": "9.1.3", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.1.3.tgz", - "integrity": "sha512-Xcpl9FqXOAYqI3j79pEtHBBnQgVXIhpULjGQa7DVb0Po+VzmSIK9kanAiWLHoRR/dbZ2qpdPshuXr8l1VaHCzw==", - "dev": true, - "requires": { - "@ungap/promise-all-settled": "1.1.2", - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.2", - "debug": "4.3.2", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.1.7", - "growl": "1.10.5", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "3.0.4", - "ms": "2.1.3", - "nanoid": "3.1.25", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "which": "2.0.2", - "workerpool": "6.1.5", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.1.0.tgz", + "integrity": "sha512-8uJR5RTC2NgpY3GrYcgpZrsEd9zKbPDpob1RezyR2upGHRQtHWofmzTMzTMSV6dru3tj5Ukt0+Vnq1qhFEEwAg==", + "dev": true, + "requires": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" } }, "ms": { @@ -3184,22 +2951,16 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, - "nanoid": { - "version": "3.1.25", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.25.tgz", - "integrity": "sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q==", - "dev": true - }, - "normalize-package-data": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", - "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "node-emoji": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.1.3.tgz", + "integrity": "sha512-E2WEOVsgs7O16zsURJ/eH8BqhF029wGpEOnv7Urwdo2wmQanOACwJQh0devF9D9RhoZru0+9JXIS0dBXIAz+lA==", "dev": true, "requires": { - "hosted-git-info": "^4.0.1", - "is-core-module": "^2.5.0", - "semver": "^7.3.4", - "validate-npm-package-license": "^3.0.1" + "@sindresorhus/is": "^4.6.0", + "char-regex": "^1.0.2", + "emojilib": "^2.4.0", + "skin-tone": "^2.0.0" } }, "normalize-path": { @@ -3208,15 +2969,6 @@ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1" - } - }, "p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -3235,85 +2987,58 @@ "p-limit": "^3.0.2" } }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "dev": true }, - "parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - } - }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true - }, - "pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true - }, - "picomatch": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", - "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true }, - "plur": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/plur/-/plur-4.0.0.tgz", - "integrity": "sha512-4UGewrYgqDFw9vV6zNV+ADmPAUAfJPKtGvb/VdpQAx25X5f3xXdGdyOEVFwkl8Hl/tl7+xbeHqSEM+D5/TirUg==", + "path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, "requires": { - "irregular-plurals": "^3.2.0" + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + } } }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true }, - "queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "dev": true }, - "quick-lru": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", - "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", "dev": true }, "randombytes": { @@ -3325,106 +3050,6 @@ "safe-buffer": "^5.1.0" } }, - "read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "dev": true, - "requires": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "dependencies": { - "hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true - }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - }, - "type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "dev": true - } - } - }, - "read-pkg-up": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", - "dev": true, - "requires": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" - }, - "dependencies": { - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true - } - } - }, "readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -3434,20 +3059,19 @@ "picomatch": "^2.2.1" } }, - "redent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "redeyed": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/redeyed/-/redeyed-2.1.1.tgz", + "integrity": "sha512-FNpGGo1DycYAdnrKFxCMmKYgo/mILAqtRYbkdQD8Ep/Hk2PQ5+aEAEx+IU713RTDmuBaH0c8P5ZozurNu5ObRQ==", "dev": true, "requires": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" + "esprima": "~4.0.0" } }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true }, "require-from-string": { @@ -3456,31 +3080,6 @@ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true }, - "resolve": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", - "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", - "dev": true, - "requires": { - "is-core-module": "^2.2.0", - "path-parse": "^1.0.6" - } - }, - "reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true - }, - "run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "requires": { - "queue-microtask": "^1.2.2" - } - }, "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -3488,61 +3087,53 @@ "dev": true }, "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "requires": { "lru-cache": "^6.0.0" } }, "serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "requires": { "randombytes": "^2.1.0" } }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "spdx-correct": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", - "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" + "shebang-regex": "^3.0.0" } }, - "spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true }, - "spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "skin-tone": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/skin-tone/-/skin-tone-2.0.0.tgz", + "integrity": "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==", "dev": true, "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" + "unicode-emoji-modifier-base": "^1.0.0" } }, - "spdx-license-ids": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz", - "integrity": "sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g==", - "dev": true - }, "string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -3554,6 +3145,17 @@ "strip-ansi": "^6.0.1" } }, + "string-width-cjs": { + "version": "npm:string-width@4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, "strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -3563,13 +3165,13 @@ "ansi-regex": "^5.0.1" } }, - "strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "strip-ansi-cjs": { + "version": "npm:strip-ansi@6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "requires": { - "min-indent": "^1.0.0" + "ansi-regex": "^5.0.1" } }, "strip-json-comments": { @@ -3588,9 +3190,9 @@ } }, "supports-hyperlinks": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz", - "integrity": "sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.0.0.tgz", + "integrity": "sha512-QBDPHyPQDRTy9ku4URNGY5Lah8PAaXs6tAAwp55sL5WCsSW7GIfdf6W5ixfziW+t7wh3GVvHyHHyQ1ESsoRvaA==", "dev": true, "requires": { "has-flag": "^4.0.0", @@ -3617,42 +3219,28 @@ "is-number": "^7.0.0" } }, - "trim-newlines": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", - "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", + "type-fest": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", + "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", "dev": true }, - "tsd": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/tsd/-/tsd-0.19.0.tgz", - "integrity": "sha512-DlYCjjRVspfSAC+9X1NUDqPj0PtsVjUSyC4/OJpCGnY2Mtg0ddc+opckmnLV3RhUBpVuAAOrz661v+QSJwINkQ==", - "dev": true, - "requires": { - "@tsd/typescript": "~4.5.2", - "eslint-formatter-pretty": "^4.1.0", - "globby": "^11.0.1", - "meow": "^9.0.0", - "path-exists": "^4.0.0", - "read-pkg-up": "^7.0.0" - } - }, - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true }, - "type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "dev": true }, - "typescript": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz", - "integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==", + "unicode-emoji-modifier-base": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz", + "integrity": "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==", "dev": true }, "uri-js": { @@ -3664,14 +3252,13 @@ "punycode": "^2.1.0" } }, - "validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "validate-npm-package-name": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.0.tgz", + "integrity": "sha512-YuKoXDAhBYxY7SfOKxHBDoSyENFeW5VvIIQp2TGQuit8gpK6MnWaQelBKxso72DoxTZfZdcP3W90LqpSkgPzLQ==", "dev": true, "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" + "builtins": "^5.0.0" } }, "which": { @@ -3684,13 +3271,64 @@ } }, "workerpool": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.1.5.tgz", - "integrity": "sha512-XdKkCK0Zqc6w3iTxLckiuJ81tiD/o5rBE/m+nXpRCB+/Sq4DqkfXZ/x0jW02DG1tGsfUGXbTJyZDP+eu67haSw==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", "dev": true }, "wrap-ansi": { - "version": "7.0.0", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "requires": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true + }, + "ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true + }, + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "requires": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + } + }, + "strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "requires": { + "ansi-regex": "^6.0.1" + } + } + } + }, + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, @@ -3700,12 +3338,6 @@ "strip-ansi": "^6.0.0" } }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -3719,24 +3351,24 @@ "dev": true }, "yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, "requires": { - "cliui": "^7.0.2", + "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "string-width": "^4.2.0", + "string-width": "^4.2.3", "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" + "yargs-parser": "^21.1.1" } }, "yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true }, "yargs-unparser": { diff --git a/package.json b/package.json index 81f0bf75e..fc3c42e3b 100644 --- a/package.json +++ b/package.json @@ -1,40 +1,43 @@ { "name": "@sinclair/typebox", - "version": "0.23.0", - "description": "JSONSchema Type Builder with Static Type Resolution for TypeScript", + "version": "0.34.41", + "description": "Json Schema Type Builder with Static Type Resolution for TypeScript", "keywords": [ - "json-schema", "typescript", - "static-types", - "runtime-typechecking" + "json-schema", + "validate", + "typecheck" ], "author": "sinclairzx81", "license": "MIT", - "main": "./typebox.js", - "types": "./typebox.d.ts", "repository": { "type": "git", "url": "https://github.com/sinclairzx81/typebox" }, "scripts": { - "clean": "hammer task clean", + "test:typescript": "hammer task test_typescript", + "test:static": "hammer task test_static", + "test:runtime": "hammer task test_runtime", + "install:local": "hammer task install_local", + "benchmark": "hammer task benchmark", + "build:to": "hammer task build_to", "build": "hammer task build", - "example": "hammer task example", - "spec": "hammer task spec", - "spec:types": "hammer task spec_types", - "spec:schemas": "hammer task spec_schemas", - "test": "npm run spec" + "test": "hammer task test", + "clean": "hammer task clean", + "format": "hammer task format", + "start": "hammer task start", + "publish": "hammer task publish", + "publish:dev": "hammer task publish_dev" }, "devDependencies": { - "@sinclair/hammer": "^0.15.8", - "@types/chai": "^4.2.22", - "@types/mocha": "^9.0.0", - "@types/node": "^16.11.9", - "ajv": "^8.8.2", + "@arethetypeswrong/cli": "^0.13.2", + "@sinclair/hammer": "^0.18.0", + "@types/mocha": "^9.1.1", + "@types/node": "^22.13.5", + "ajv": "^8.12.0", "ajv-formats": "^2.1.1", - "chai": "^4.3.4", - "mocha": "^9.1.3", - "tsd": "^0.19.0", - "typescript": "^4.5.2" + "mocha": "^11.1.0", + "prettier": "^2.7.1", + "typescript": "^5.9.2" } } diff --git a/readme.md b/readme.md index 92b344759..18bbc55e7 100644 --- a/readme.md +++ b/readme.md @@ -2,9 +2,17 @@

TypeBox

-

JSON Schema Type Builder with Static Type Resolution for TypeScript

+

Json Schema Type Builder with Static Type Resolution for TypeScript

-[![npm version](https://badge.fury.io/js/%40sinclair%2Ftypebox.svg)](https://badge.fury.io/js/%40sinclair%2Ftypebox) [![GitHub CI](https://github.com/sinclairzx81/typebox/workflows/GitHub%20CI/badge.svg)](https://github.com/sinclairzx81/typebox/actions) + + +
+
+ +[![npm version](https://badge.fury.io/js/%40sinclair%2Ftypebox.svg)](https://badge.fury.io/js/%40sinclair%2Ftypebox) +[![Downloads](https://img.shields.io/npm/dm/%40sinclair%2Ftypebox.svg)](https://www.npmjs.com/package/%40sinclair%2Ftypebox) +[![Build](https://github.com/sinclairzx81/typebox/actions/workflows/build.yml/badge.svg)](https://github.com/sinclairzx81/typebox/actions/workflows/build.yml) +[![License](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) @@ -12,64 +20,115 @@ ## Install -#### Node - ```bash $ npm install @sinclair/typebox --save ``` -#### Deno +## Example ```typescript -import { Static, Type } from 'https://deno.land/x/typebox/src/typebox.ts' +import { Type, type Static } from '@sinclair/typebox' + +const T = Type.Object({ // const T = { + x: Type.Number(), // type: 'object', + y: Type.Number(), // required: ['x', 'y', 'z'], + z: Type.Number() // properties: { +}) // x: { type: 'number' }, + // y: { type: 'number' }, + // z: { type: 'number' } + // } + // } + +type T = Static // type T = { + // x: number, + // y: number, + // z: number + // } ``` -## Usage - -```typescript -import { Static, Type } from '@sinclair/typebox' - -const T = Type.String() // const T = { "type": "string" } - -type T = Static // type T = string -``` ## Overview -TypeBox is a library that builds in-memory JSON Schema objects that can be statically resolved to TypeScript types. The schemas produced by this library are designed to match the static type checking rules of the TypeScript compiler. TypeBox allows one to create a unified type that can be statically checked by the TypeScript compiler and runtime asserted using standard JSON Schema validation. +TypeBox is a runtime type builder that creates in-memory Json Schema objects that infer as TypeScript types. The schematics produced by this library are designed to match the static type checking rules of the TypeScript compiler. TypeBox offers a unified type that can be statically checked by TypeScript 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 to validate schemas built with this library. - -Requires TypeScript 4.3.5 and above. +This library is designed to allow Json Schema to compose similar to how types compose within TypeScript's type system. It can be used as a simple tool to build up complex schematics or integrated into REST and RPC services to help validate data received over the wire. License MIT ## Contents -- [Install](#Install) -- [Overview](#Overview) -- [Example](#Example) -- [Types](#Types) -- [Modifiers](#Modifiers) -- [Options](#Options) -- [Generic Types](#Generic-Types) -- [Reference Types](#Reference-Types) -- [Recursive Types](#Recursive-Types) -- [Extended Types](#Extended-Types) -- [Strict](#Strict) -- [Validation](#Validation) -- [OpenAPI](#OpenAPI) - - +- [Install](#install) +- [Overview](#overview) +- [Usage](#usage) +- [Types](#types) + - [Json](#types-json) + - [JavaScript](#types-javascript) + - [Options](#types-options) + - [Properties](#types-properties) + - [Generics](#types-generics) + - [Recursive](#types-recursive) + - [Modules](#types-modules) + - [Template Literal](#types-template-literal) + - [Indexed](#types-indexed) + - [Mapped](#types-mapped) + - [Conditional](#types-conditional) + - [Transform](#types-transform) + - [Guard](#types-guard) + - [Unsafe](#types-unsafe) +- [Values](#values) + - [Assert](#values-assert) + - [Create](#values-create) + - [Clone](#values-clone) + - [Check](#values-check) + - [Convert](#values-convert) + - [Default](#values-default) + - [Clean](#values-clean) + - [Cast](#values-cast) + - [Decode](#values-decode) + - [Encode](#values-decode) + - [Parse](#values-parse) + - [Equal](#values-equal) + - [Hash](#values-hash) + - [Diff](#values-diff) + - [Patch](#values-patch) + - [Errors](#values-errors) + - [Mutate](#values-mutate) + - [Pointer](#values-pointer) +- [Syntax](#syntax) + - [Create](#syntax-create) + - [Parameters](#syntax-parameters) + - [Generics](#syntax-generics) + - [Options](#syntax-options) + - [NoInfer](#syntax-no-infer) +- [TypeRegistry](#typeregistry) + - [Type](#typeregistry-type) + - [Format](#typeregistry-format) +- [TypeCheck](#typecheck) + - [Ajv](#typecheck-ajv) + - [TypeCompiler](#typecheck-typecompiler) +- [TypeMap](#typemap) + - [Usage](#typemap-usage) +- [TypeSystem](#typesystem) + - [Policies](#typesystem-policies) +- [Error Function](#error-function) +- [Workbench](#workbench) +- [Codegen](#codegen) +- [Ecosystem](#ecosystem) +- [Benchmark](#benchmark) + - [Compile](#benchmark-compile) + - [Validate](#benchmark-validate) + - [Compression](#benchmark-compression) +- [Contribute](#contribute) + + -## Example +## Usage -The following demonstrates TypeBox's general usage. +The following shows general usage. ```typescript - -import { Static, Type } from '@sinclair/typebox' +import { Type, type Static } from '@sinclair/typebox' //-------------------------------------------------------------------------------------------- // @@ -78,9 +137,9 @@ import { Static, Type } from '@sinclair/typebox' //-------------------------------------------------------------------------------------------- type T = { - id: string, - name: string, - timestamp: number + id: string, + name: string, + timestamp: number } //-------------------------------------------------------------------------------------------- @@ -89,25 +148,25 @@ type T = { // //-------------------------------------------------------------------------------------------- -const T = Type.Object({ // const T = { - id: Type.String(), // type: 'object', - name: Type.String(), // properties: { - timestamp: Type.Integer() // id: { -}) // type: 'string' - // }, - // name: { - // type: 'string' - // }, - // timestamp: { - // type: 'integer' - // } - // }, - // required: [ - // "id", - // "name", - // "timestamp" - // ] - // } +const T = Type.Object({ // const T = { + id: Type.String(), // type: 'object', + name: Type.String(), // properties: { + timestamp: Type.Integer() // id: { +}) // type: 'string' + // }, + // name: { + // type: 'string' + // }, + // timestamp: { + // type: 'integer' + // } + // }, + // required: [ + // 'id', + // 'name', + // 'timestamp' + // ] + // } //-------------------------------------------------------------------------------------------- // @@ -115,117 +174,116 @@ const T = Type.Object({ // const T = { // //-------------------------------------------------------------------------------------------- -type T = Static // type T = { - // id: string, - // name: string, - // timestamp: number - // } +type T = Static // type T = { + // id: string, + // name: string, + // timestamp: number + // } //-------------------------------------------------------------------------------------------- // -// ... then use the type both as JSON schema and as a TypeScript type. +// ... or use the type to parse JavaScript values. // //-------------------------------------------------------------------------------------------- -function receive(value: T) { // ... as a Type +import { Value } from '@sinclair/typebox/value' - if(JSON.validate(T, value)) { // ... as a Schema - - // ok... - } -} +const R = Value.Parse(T, value) // const R: { + // id: string, + // name: string, + // timestamp: number + // } ``` - + ## Types -The following table outlines the TypeBox mappings between TypeScript and JSON schema. +TypeBox types are Json Schema fragments that compose into more complex types. Each fragment is structured such that any Json Schema compliant validator can runtime assert a value the same way TypeScript will statically assert a type. TypeBox offers a set of Json Types which are used to create Json Schema compliant schematics as well as a JavaScript type set used to create schematics for constructs native to JavaScript. + + + +### Json Types + +The following table lists the supported Json types. These types are fully compatible with the Json Schema Draft 7 specification. ```typescript ┌────────────────────────────────┬─────────────────────────────┬────────────────────────────────┐ -│ TypeBox │ TypeScript │ JSON Schema │ -│ │ │ │ +│ TypeBox │ TypeScript │ Json Schema │ +│ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Any() │ type T = any │ const T = { } │ -│ │ │ │ +│ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Unknown() │ type T = unknown │ const T = { } │ -│ │ │ │ +│ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.String() │ type T = string │ const T = { │ -│ │ │ type: 'string' │ +│ │ │ type: 'string' │ │ │ │ } │ -│ │ │ │ +│ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Number() │ type T = number │ const T = { │ -│ │ │ type: 'number' │ +│ │ │ type: 'number' │ │ │ │ } │ -│ │ │ │ +│ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Integer() │ type T = number │ const T = { │ -│ │ │ type: 'integer' │ +│ │ │ type: 'integer' │ │ │ │ } │ -│ │ │ │ +│ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Boolean() │ type T = boolean │ const T = { │ -│ │ │ type: 'boolean' │ +│ │ │ type: 'boolean' │ │ │ │ } │ -│ │ │ │ +│ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Null() │ type T = null │ const T = { │ -│ │ │ type: 'null' │ -│ │ │ } │ -│ │ │ │ -├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ -│ const T = Type.RegEx(/foo/) │ type T = string │ const T = { │ -│ │ │ type: 'string', │ -│ │ │ pattern: 'foo' │ +│ │ │ type: 'null' │ │ │ │ } │ -│ │ │ │ +│ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Literal(42) │ type T = 42 │ const T = { │ -│ │ │ const: 42 │ -│ │ │ type: 'number' │ +│ │ │ const: 42, │ +│ │ │ type: 'number' │ │ │ │ } │ -│ │ │ │ +│ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Array( │ type T = number[] │ const T = { │ -│ Type.Number() │ │ type: 'array', │ -│ ) │ │ items: { │ -│ │ │ type: 'number' │ -│ │ │ } │ +│ Type.Number() │ │ type: 'array', │ +│ ) │ │ items: { │ +│ │ │ type: 'number' │ +│ │ │ } │ │ │ │ } │ -│ │ │ │ +│ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Object({ │ type T = { │ const T = { │ -│ x: Type.Number(), │ x: number, │ type: 'object', │ -│ y: Type.Number() │ y: number │ properties: { │ -│ }) │ } │ x: { │ -│ │ │ type: 'number' │ -│ │ │ }, │ -│ │ │ y: { │ -│ │ │ type: 'number' │ -│ │ │ } │ -│ │ │ }, │ -│ │ │ required: ['x', 'y'] │ -│ │ │ } │ -│ │ │ │ +│ x: Type.Number(), │ x: number, │ type: 'object', │ +│ y: Type.Number() │ y: number │ required: ['x', 'y'], │ +│ }) │ } │ properties: { │ +│ │ │ x: { │ +│ │ │ type: 'number' │ +│ │ │ }, │ +│ │ │ y: { │ +│ │ │ type: 'number' │ +│ │ │ } │ +│ │ │ } │ +│ │ │ } │ +│ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Tuple([ │ type T = [number, number] │ const T = { │ -│ Type.Number(), │ │ type: 'array', │ -│ Type.Number() │ │ items: [ │ -│ ]) │ │ { │ -│ │ │ type: 'number' │ -│ │ │ }, { │ -│ │ │ type: 'number' │ -│ │ │ } │ -│ │ │ ], │ -│ │ │ additionalItems: false, │ -│ │ │ minItems: 2, │ -│ │ │ maxItems: 2, │ -│ │ │ } │ -│ │ │ │ +│ Type.Number(), │ │ type: 'array', │ +│ Type.Number() │ │ items: [{ │ +│ ]) │ │ type: 'number' │ +│ │ │ }, { │ +│ │ │ type: 'number' │ +│ │ │ }], │ +│ │ │ additionalItems: false, │ +│ │ │ minItems: 2, │ +│ │ │ maxItems: 2 │ +│ │ │ } │ +│ │ │ │ +│ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ enum Foo { │ enum Foo { │ const T = { │ │ A, │ A, │ anyOf: [{ │ @@ -238,551 +296,1566 @@ The following table outlines the TypeBox mappings between TypeScript and JSON sc │ │ │ } │ │ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ +│ const T = Type.Const({ │ type T = { │ const T = { │ +│ x: 1, │ readonly x: 1, │ type: 'object', │ +│ y: 2, │ readonly y: 2 │ required: ['x', 'y'], │ +│ } as const) │ } │ properties: { │ +│ │ │ x: { │ +│ │ │ type: 'number', │ +│ │ │ const: 1 │ +│ │ │ }, │ +│ │ │ y: { │ +│ │ │ type: 'number', │ +│ │ │ const: 2 │ +│ │ │ } │ +│ │ │ } │ +│ │ │ } │ +│ │ │ │ +├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.KeyOf( │ type T = keyof { │ const T = { │ -│ Type.Object({ │ x: number, │ enum: ['x', 'y'], │ -│ x: Type.Number(), │ y: number │ type: 'string' │ -│ y: Type.Number() │ } │ } │ -│ }) │ │ │ -│ ) │ │ │ -│ │ │ │ +│ Type.Object({ │ x: number, │ anyOf: [{ │ +│ x: Type.Number(), │ y: number │ type: 'string', │ +│ y: Type.Number() │ } │ const: 'x' │ +│ }) │ │ }, { │ +│ ) │ │ type: 'string', │ +│ │ │ const: 'y' │ +│ │ │ }] │ +│ │ │ } │ +│ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Union([ │ type T = string | number │ const T = { │ -│ Type.String(), │ │ anyOf: [{ │ -│ Type.Number() │ │ type: 'string' │ -│ ]) │ │ }, { │ -│ │ │ type: 'number' │ -│ │ │ }] │ +│ Type.String(), │ │ anyOf: [{ │ +│ Type.Number() │ │ type: 'string' │ +│ ]) │ │ }, { │ +│ │ │ type: 'number' │ +│ │ │ }] │ │ │ │ } │ -│ │ │ │ +│ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Intersect([ │ type T = { │ const T = { │ -│ 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' │ -│ │ │ } │ -│ │ │ } │ -│ │ │ } │ -│ │ │ │ +│ Type.Object({ │ x: number │ allOf: [{ │ +│ x: Type.Number() │ } & { │ type: 'object', │ +│ }), │ y: number │ required: ['x'], │ +│ Type.Object({ │ } │ properties: { │ +│ y: Type.Number() │ │ x: { │ +│ }) │ │ type: 'number' │ +│ ]) │ │ } │ +│ │ │ } │ +│ │ │ }, { │ +│ │ │ type: 'object', | +│ │ │ required: ['y'], │ +│ │ │ properties: { │ +│ │ │ y: { │ +│ │ │ type: 'number' │ +│ │ │ } │ +│ │ │ } │ +│ │ │ }] │ +│ │ │ } │ +│ │ │ │ +├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ +│ const T = Type.Composite([ │ type T = { │ const T = { │ +│ Type.Object({ │ x: number, │ type: 'object', │ +│ x: Type.Number() │ y: number │ required: ['x', 'y'], │ +│ }), │ } │ properties: { │ +│ Type.Object({ │ │ x: { │ +│ y: Type.Number() │ │ type: 'number' │ +│ }) │ │ }, │ +│ ]) │ │ y: { │ +│ │ │ type: 'number' │ +│ │ │ } │ +│ │ │ } │ +│ │ │ } │ +│ │ │ │ +├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ +│ const T = Type.Never() │ type T = never │ const T = { │ +│ │ │ not: {} │ +│ │ │ } │ +│ │ │ │ +├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ +│ const T = Type.Not( | type T = unknown │ const T = { │ +│ Type.String() │ │ not: { │ +│ ) │ │ type: 'string' │ +│ │ │ } │ +│ │ │ } │ +├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ +│ const T = Type.Extends( │ type T = │ const T = { │ +│ Type.String(), │ string extends number │ const: false, │ +│ Type.Number(), │ ? true │ type: 'boolean' │ +│ Type.Literal(true), │ : false │ } │ +│ Type.Literal(false) │ │ │ +│ ) │ │ │ +│ │ │ │ +├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ +│ const T = Type.Extract( │ type T = Extract< │ const T = { │ +│ Type.Union([ │ string | number, │ type: 'string' │ +│ Type.String(), │ string │ } │ +│ Type.Number(), │ > │ │ +│ ]), │ │ │ +│ Type.String() │ │ │ +│ ) │ │ │ +│ │ │ │ +├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ +│ const T = Type.Exclude( │ type T = Exclude< │ const T = { │ +│ Type.Union([ │ string | number, │ type: 'number' │ +│ Type.String(), │ string │ } │ +│ Type.Number(), │ > │ │ +│ ]), │ │ │ +│ Type.String() │ │ │ +│ ) │ │ │ +│ │ │ │ +├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ +│ const T = Type.Mapped( │ type T = { │ const T = { │ +│ Type.Union([ │ [_ in 'x' | 'y'] : number │ type: 'object', │ +│ Type.Literal('x'), │ } │ required: ['x', 'y'], │ +│ Type.Literal('y') │ │ properties: { │ +│ ]), │ │ x: { │ +│ () => Type.Number() │ │ type: 'number' │ +│ ) │ │ }, │ +│ │ │ y: { │ +│ │ │ type: 'number' │ +│ │ │ } │ +│ │ │ } │ +│ │ │ } │ +│ │ │ │ +├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ +│ const U = Type.Union([ │ type U = 'open' | 'close' │ const T = { │ +│ Type.Literal('open'), │ │ type: 'string', │ +│ Type.Literal('close') │ type T = `on${U}` │ pattern: '^on(open|close)$' │ +│ ]) │ │ } │ +│ │ │ │ +│ const T = Type │ │ │ +│ .TemplateLiteral([ │ │ │ +│ Type.Literal('on'), │ │ │ +│ U │ │ │ +│ ]) │ │ │ +│ │ │ │ +├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ +│ const T = Type.Record( │ type T = Record< │ const T = { │ +│ Type.String(), │ string, │ type: 'object', │ +│ Type.Number() │ number │ patternProperties: { │ +│ ) │ > │ '^.*$': { │ +│ │ │ type: 'number' │ +│ │ │ } │ +│ │ │ } │ +│ │ │ } │ +│ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Partial( │ type T = Partial<{ │ const T = { │ -│ Type.Object({ │ x: number, │ type: 'object', │ -│ x: Type.Number(), │ y: number │ properties: { │ -│ y: Type.Number() | }> │ x: { │ -│ }) │ │ type: 'number' │ +│ Type.Object({ │ x: number, │ type: 'object', │ +│ x: Type.Number(), │ y: number │ properties: { │ +│ y: Type.Number() | }> │ x: { │ +│ }) │ │ type: 'number' │ │ ) │ │ }, │ │ │ │ y: { │ -│ │ │ type: 'number' │ +│ │ │ type: 'number' │ │ │ │ } │ │ │ │ } │ │ │ │ } │ -│ │ │ │ +│ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Required( │ type T = Required<{ │ const T = { │ -│ Type.Object({ │ x?: number, │ type: 'object', │ -│ x: Type.Optional( │ y?: number │ properties: { │ -│ Type.Number() | }> │ x: { │ -│ ), │ │ type: 'number' │ -│ y: Type.Optional( │ │ }, │ -│ Type.Number() │ │ y: { │ -│ ) │ │ type: 'number' │ -│ }) │ │ } │ -│ ) │ │ }, │ -│ │ │ required: ['x', 'y'] │ -│ │ │ } │ -│ │ │ │ +│ Type.Object({ │ x?: number, │ type: 'object', │ +│ x: Type.Optional( │ y?: number │ required: ['x', 'y'], │ +│ Type.Number() | }> │ properties: { │ +│ ), │ │ x: { │ +│ y: Type.Optional( │ │ type: 'number' │ +│ Type.Number() │ │ }, │ +│ ) │ │ y: { │ +│ }) │ │ type: 'number' │ +│ ) │ │ } │ +│ │ │ } │ +│ │ │ } │ +│ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Pick( │ type T = Pick<{ │ const T = { │ -│ Type.Object({ │ x: number, │ type: 'object', │ -│ x: Type.Number(), │ y: number │ properties: { │ -│ y: Type.Number(), | }, 'x'> │ x: { │ -│ }), ['x'] │ │ type: 'number' │ -│ ) │ │ } │ -│ │ │ }, │ -│ │ │ required: ['x'] │ +│ Type.Object({ │ x: number, │ type: 'object', │ +│ x: Type.Number(), │ y: number │ required: ['x'], │ +│ y: Type.Number() │ }, 'x'> │ properties: { │ +│ }), ['x'] | │ x: { │ +│ ) │ │ type: 'number' │ +│ │ │ } │ +│ │ │ } │ │ │ │ } │ │ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Omit( │ type T = Omit<{ │ const T = { │ -│ Type.Object({ │ x: number, │ type: 'object', │ -│ x: Type.Number(), │ y: number │ properties: { │ -│ y: Type.Number(), | }, 'x'> │ y: { │ -│ }), ['x'] │ │ type: 'number' │ -│ ) │ │ } │ -│ │ │ }, │ -│ │ │ required: ['y'] │ +│ Type.Object({ │ x: number, │ type: 'object', │ +│ x: Type.Number(), │ y: number │ required: ['y'], │ +│ y: Type.Number() │ }, 'x'> │ properties: { │ +│ }), ['x'] | │ y: { │ +│ ) │ │ type: 'number' │ +│ │ │ } │ +│ │ │ } │ │ │ │ } │ │ │ │ │ -└────────────────────────────────┴─────────────────────────────┴────────────────────────────────┘ -``` - - -### Modifiers - -TypeBox provides modifiers that can be applied to an objects properties. This allows for `optional` and `readonly` to be applied to that property. The following table illustates how they map between TypeScript and JSON Schema. - -```typescript -┌────────────────────────────────┬─────────────────────────────┬────────────────────────────────┐ -│ TypeBox │ TypeScript │ JSON Schema │ -│ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ -│ const T = Type.Object({ │ type T = { │ const T = { │ -│ name: Type.Optional( │ name?: string, │ type: 'object', │ -│ Type.String(), │ } │ properties: { │ -│ ) │ │ name: { │ -│ }) │ │ type: 'string' │ -│ │ │ } │ -│ │ │ } │ -│ │ │ } │ -│ │ │ │ +│ const T = Type.Index( │ type T = { │ const T = { │ +│ Type.Object({ │ x: number, │ type: 'number' │ +│ x: Type.Number(), │ y: string │ } │ +│ y: Type.String() │ }['x'] │ │ +│ }), ['x'] │ │ │ +│ ) │ │ │ +│ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ -│ const T = Type.Object({ │ type T = { │ const T = { │ -│ name: Type.Readonly( │ readonly name: string, │ type: 'object', │ -│ Type.String(), │ } │ properties: { │ -│ ) │ │ name: { │ -│ }) │ │ type: 'string' │ -│ │ │ } │ -│ │ │ }, │ -│ │ │ required: ['name'] │ -│ │ │ } │ -│ │ │ │ +│ const A = Type.Tuple([ │ type A = [0, 1] │ const T = { │ +│ Type.Literal(0), │ type B = [2, 3] │ type: 'array', │ +│ Type.Literal(1) │ type T = [ │ items: [ │ +│ ]) │ ...A, │ { const: 0 }, │ +│ const B = Type.Tuple([ │ ...B │ { const: 1 }, │ +| Type.Literal(2), │ ] │ { const: 2 }, │ +| Type.Literal(3) │ │ { const: 3 } │ +│ ]) │ │ ], │ +│ const T = Type.Tuple([ │ │ additionalItems: false, │ +| ...Type.Rest(A), │ │ minItems: 4, │ +| ...Type.Rest(B) │ │ maxItems: 4 │ +│ ]) │ │ } │ +│ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ -│ const T = Type.Object({ │ type T = { │ const T = { │ -│ name: Type.ReadonlyOptional( │ readonly name?: string, │ type: 'object', │ -│ Type.String(), │ } │ properties: { │ -│ ) │ │ name: { │ -│ }) │ │ type: 'string' │ -│ │ │ } │ -│ │ │ } │ -│ │ │ } │ -│ │ │ │ +│ const T = Type.Uncapitalize( │ type T = Uncapitalize< │ const T = { │ +│ Type.Literal('Hello') │ 'Hello' │ type: 'string', │ +│ ) │ > │ const: 'hello' │ +│ │ │ } │ +│ │ │ │ +├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ +│ const T = Type.Capitalize( │ type T = Capitalize< │ const T = { │ +│ Type.Literal('hello') │ 'hello' │ type: 'string', │ +│ ) │ > │ const: 'Hello' │ +│ │ │ } │ +│ │ │ │ +├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ +│ const T = Type.Uppercase( │ type T = Uppercase< │ const T = { │ +│ Type.Literal('hello') │ 'hello' │ type: 'string', │ +│ ) │ > │ const: 'HELLO' │ +│ │ │ } │ +│ │ │ │ +├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ +│ const T = Type.Lowercase( │ type T = Lowercase< │ const T = { │ +│ Type.Literal('HELLO') │ 'HELLO' │ type: 'string', │ +│ ) │ > │ const: 'hello' │ +│ │ │ } │ +│ │ │ │ +├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ +│ const R = Type.Ref('T') │ type R = unknown │ const R = { $ref: 'T' } │ +│ │ │ │ └────────────────────────────────┴─────────────────────────────┴────────────────────────────────┘ ``` - - -### Options - -You can pass additional JSON schema options on the last argument of any given type. The following are some examples. - -```typescript -// string must be an email -const T = Type.String({ format: 'email' }) - -// number must be a multiple of 2 -const T = Type.Number({ multipleOf: 2 }) - -// array must have at least 5 integer values -const T = Type.Array(Type.Integer(), { minItems: 5 }) -``` - - -### Generic Types - -Generic types can be created using functions. The following creates a generic `Nullable` type. - -```typescript -import { Type, Static, TSchema } from '@sinclair/typebox' - -// type Nullable = T | null - -const Nullable = (type: T) => Type.Union([type, Type.Null()]) - -const T = Nullable(Type.String()) // const T = { - // "anyOf": [{ - // type: 'string' - // }, { - // type: 'null' - // }] - // } - -type T = Static // type T = string | null - -const U = Nullable(Type.Number()) // const U = { - // "anyOf": [{ - // type: 'number' - // }, { - // type: 'null' - // }] - // } - -type U = Static // type U = number | null -``` - - - -### Reference Types - -Types can be referenced with `Type.Ref(...)`. To reference a type, the target type must specify an `$id`. - -```typescript -const T = Type.String({ $id: 'T' }) // const T = { - // $id: 'T', - // type: 'string' - // } - -const R = Type.Ref(T) // const R = { - // $ref: 'T' - // } -``` - -It can be helpful to organize shared referenced types under a common namespace. The `Type.Namespace(...)` function can be used to create a shared definition container for related types. The following creates a `Math3D` container and a `Vertex` structure that references types in the container. - -```typescript -const Math3D = Type.Namespace({ // const Math3D = { - Vector4: Type.Object({ // $id: 'Math3D', - x: Type.Number(), // $defs: { - y: Type.Number(), // Vector4: { - z: Type.Number(), // type: 'object', - w: Type.Number() // properties: { - }), // x: { type: 'number' }, - Vector3: Type.Object({ // y: { type: 'number' }, - x: Type.Number(), // z: { type: 'number' }, - y: Type.Number(), // w: { type: 'number' } - z: Type.Number() // }, - }), // required: ['x', 'y', 'z', 'w'] - Vector2: Type.Object({ // }, - x: Type.Number(), // Vector3: { - y: Type.Number() // type: 'object', - }) // properties: { -}, { $id: 'Math3D' }) // x: { 'type': 'number' }, - // y: { 'type': 'number' }, - // z: { 'type': 'number' } - // }, - // required: ['x', 'y', 'z'] - // }, - // Vector2: { - // type: 'object', - // properties: { - // x: { 'type': 'number' }, - // y: { 'type': 'number' }, - // }, - // required: ['x', 'y'] - // } - // } - // } - -const Vertex = Type.Object({ // const Vertex = { - position: Type.Ref(Math3D, 'Vector4'), // type: 'object', - normal: Type.Ref(Math3D, 'Vector3'), // properties: { - uv: Type.Ref(Math3D, 'Vector2') // position: { $ref: 'Math3D#/$defs/Vector4' }, -}) // normal: { $ref: 'Math3D#/$defs/Vector3' }, - // uv: { $ref: 'Math3D#/$defs/Vector2' } - // }, - // required: ['position', 'normal', 'uv'] - // } -``` - - - -### Recursive Types - -Recursive types can be created with the `Type.Rec(...)` function. The following creates a `Node` type that contains an array of inner Nodes. Note that due to current restrictions on TypeScript inference, it is not possible for TypeBox to statically infer for recursive types. TypeBox will infer the inner recursive type as `any`. - -```typescript -const Node = Type.Rec(Self => Type.Object({ // const Node = { - id: Type.String(), // $id: 'Node', - nodes: Type.Array(Self), // $ref: 'Node#/$defs/self', -}), { $id: 'Node' }) // $defs: { - // self: { - // type: 'object', - // properties: { - // id: { - // type: 'string' - // }, - // nodes: { - // type: 'array', - // items: { - // $ref: 'Node#/$defs/self' - // } - // } - // } - // } - // } - -type Node = Static // type Node = { - // id: string - // nodes: any[] - // - -function visit(node: Node) { - for(const inner of node.nodes) { - visit(inner as Node) // Assert inner as Node - } -} -``` - - + -### Extended Types +### JavaScript Types -In addition to JSON schema types, TypeBox provides several extended types that allow for `function` and `constructor` types to be composed. These additional types are not valid JSON Schema and will not validate using typical JSON Schema validation. However, these types can be used to frame JSON schema and describe callable interfaces that may receive JSON validated data. These types are as follows. +TypeBox provides an extended type set that can be used to create schematics for common JavaScript constructs. These types can not be used with any standard Json Schema validator; but can be used to frame schematics for interfaces that may receive Json validated data. JavaScript types are prefixed with the `[JavaScript]` JSDoc comment for convenience. The following table lists the supported types. ```typescript ┌────────────────────────────────┬─────────────────────────────┬────────────────────────────────┐ │ TypeBox │ TypeScript │ Extended Schema │ -│ │ │ │ +│ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Constructor([ │ type T = new ( │ const T = { │ -│ Type.String(), │ arg0: string, │ type: 'constructor' │ -│ Type.Number(), │ arg1: number │ arguments: [{ │ -│ ], Type.Boolean()) │ ) => boolean │ type: 'string' │ +│ Type.String(), │ arg0: string, │ type: 'Constructor', │ +│ Type.Number() │ arg0: number │ parameters: [{ │ +│ ], Type.Boolean()) │ ) => boolean │ type: 'string' │ │ │ │ }, { │ -│ │ │ type: 'number' │ +│ │ │ type: 'number' │ │ │ │ }], │ │ │ │ returns: { │ -│ │ │ type: 'boolean' │ +│ │ │ type: 'boolean' │ │ │ │ } │ │ │ │ } │ -│ │ │ │ +│ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Function([ │ type T = ( │ const T = { │ -| Type.String(), │ arg0: string, │ type : 'function', │ -│ Type.Number(), │ arg1: number │ arguments: [{ │ -│ ], Type.Boolean()) │ ) => boolean │ type: 'string' │ +| Type.String(), │ arg0: string, │ type: 'Function', │ +│ Type.Number() │ arg1: number │ parameters: [{ │ +│ ], Type.Boolean()) │ ) => boolean │ type: 'string' │ │ │ │ }, { │ -│ │ │ type: 'number' │ +│ │ │ type: 'number' │ │ │ │ }], │ │ │ │ returns: { │ -│ │ │ type: 'boolean' │ +│ │ │ type: 'boolean' │ │ │ │ } │ │ │ │ } │ -│ │ │ │ +│ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Promise( │ type T = Promise │ const T = { │ -│ Type.String() │ │ type: 'promise', │ +│ Type.String() │ │ type: 'Promise', │ │ ) │ │ item: { │ -│ │ │ type: 'string' │ +│ │ │ type: 'string' │ +│ │ │ } │ +│ │ │ } │ +│ │ │ │ +├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ +│ const T = │ type T = │ const T = { │ +│ Type.AsyncIterator( │ AsyncIterableIterator< │ type: 'AsyncIterator', │ +│ Type.String() │ string │ items: { │ +│ ) │ > │ type: 'string' │ +│ │ │ } │ +│ │ │ } │ +│ │ │ │ +├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ +│ const T = Type.Iterator( │ type T = │ const T = { │ +│ Type.String() │ IterableIterator │ type: 'Iterator', │ +│ ) │ │ items: { │ +│ │ │ type: 'string' │ │ │ │ } │ │ │ │ } │ -│ │ │ │ +│ │ │ │ +├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ +│ const T = Type.RegExp(/abc/i) │ type T = string │ const T = { │ +│ │ │ type: 'RegExp' │ +│ │ │ source: 'abc' │ +│ │ │ flags: 'i' │ +│ │ │ } │ +│ │ │ │ +├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ +│ const T = Type.Uint8Array() │ type T = Uint8Array │ const T = { │ +│ │ │ type: 'Uint8Array' │ +│ │ │ } │ +│ │ │ │ +├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ +│ const T = Type.Date() │ type T = Date │ const T = { │ +│ │ │ type: 'Date' │ +│ │ │ } │ +│ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Undefined() │ type T = undefined │ const T = { │ │ │ │ type: 'undefined' │ │ │ │ } │ -│ │ │ │ +│ │ │ │ +├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ +│ const T = Type.Symbol() │ type T = symbol │ const T = { │ +│ │ │ type: 'symbol' │ +│ │ │ } │ +│ │ │ │ +├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ +│ const T = Type.BigInt() │ type T = bigint │ const T = { │ +│ │ │ type: 'bigint' │ +│ │ │ } │ +│ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Void() │ type T = void │ const T = { │ │ │ │ type: 'void' │ │ │ │ } │ -│ │ │ │ +│ │ │ │ └────────────────────────────────┴─────────────────────────────┴────────────────────────────────┘ ``` - + -### Strict +### Options -TypeBox includes the properties `kind` and `modifier` on each underlying schema. These properties are used to help TypeBox statically resolve the schemas to the appropriate TypeScript type as well as apply the appropriate modifiers to an objects properties (such as optional). These properties are not strictly valid JSON schema so in some cases it may be desirable to omit them. TypeBox provides a `Type.Strict()` function that will omit these properties if nessasary. +You can pass Json Schema options on the last argument of any given type. Option hints specific to each type are provided for convenience. ```typescript -const T = Type.Object({ // const T = { - name: Type.Optional(Type.String()) // kind: Symbol(ObjectKind), -}) // type: 'object', - // properties: { - // name: { - // kind: Symbol(StringKind), - // type: 'string', - // modifier: Symbol(OptionalModifier) - // } - // } - // } - -const U = Type.Strict(T) // const U = { - // type: 'object', - // properties: { - // name: { - // type: 'string' - // } - // } - // } +// String must be an email +const T = Type.String({ // const T = { + format: 'email' // type: 'string', +}) // format: 'email' + // } + +// Number must be a multiple of 2 +const T = Type.Number({ // const T = { + multipleOf: 2 // type: 'number', +}) // multipleOf: 2 + // } + +// Array must have at least 5 integer values +const T = Type.Array(Type.Integer(), { // const T = { + minItems: 5 // type: 'array', +}) // minItems: 5, + // items: { + // type: 'integer' + // } + // } ``` - + -### Validation +### Properties -TypeBox does not provide JSON schema validation functionality, so users will need to select an appropriate JSON Schema validator for their language or framework. TypeBox targets JSON Schema draft `2019-09` so any validator capable of draft `2019-09` should be fine. A good library to use for validation in JavaScript environments is [AJV](https://www.npmjs.com/package/ajv). The following example shows setting up AJV 7 to work with TypeBox. +Object properties can be modified with Readonly and Optional. The following table shows how these modifiers map between TypeScript and Json Schema. -```bash -$ npm install ajv ajv-formats --save +```typescript +┌────────────────────────────────┬─────────────────────────────┬────────────────────────────────┐ +│ TypeBox │ TypeScript │ Json Schema │ +│ │ │ │ +├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ +│ const T = Type.Object({ │ type T = { │ const T = { │ +│ name: Type.ReadonlyOptional( │ readonly name?: string │ type: 'object', │ +│ Type.String() │ } │ properties: { │ +│ ) │ │ name: { │ +│ }) │ │ type: 'string' │ +│ │ │ } │ +│ │ │ } │ +│ │ │ } │ +│ │ │ │ +├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ +│ const T = Type.Object({ │ type T = { │ const T = { │ +│ name: Type.Readonly( │ readonly name: string │ type: 'object', │ +│ Type.String() │ } │ properties: { │ +│ ) │ │ name: { │ +│ }) │ │ type: 'string' │ +│ │ │ } │ +│ │ │ }, │ +│ │ │ required: ['name'] │ +│ │ │ } │ +│ │ │ │ +├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ +│ const T = Type.Object({ │ type T = { │ const T = { │ +│ name: Type.Optional( │ name?: string │ type: 'object', │ +│ Type.String() │ } │ properties: { │ +│ ) │ │ name: { │ +│ }) │ │ type: 'string' │ +│ │ │ } │ +│ │ │ } │ +│ │ │ } │ +│ │ │ │ +└────────────────────────────────┴─────────────────────────────┴────────────────────────────────┘ ``` -```typescript -//-------------------------------------------------------------------------------------------- -// -// Import the 2019 compliant validator from AJV -// -//-------------------------------------------------------------------------------------------- + -import { Type } from '@sinclair/typebox' -import addFormats from 'ajv-formats' -import Ajv from 'ajv/dist/2019' +### Generic Types -//-------------------------------------------------------------------------------------------- -// -// Setup AJV validator with the following options and formats -// -//-------------------------------------------------------------------------------------------- +Generic types can be created with generic functions. -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') +```typescript +const Nullable = (T: T) => { // type Nullable = T | null + return Type.Union([T, Type.Null()]) +} -//-------------------------------------------------------------------------------------------- -// -// Create a TypeBox type -// -//-------------------------------------------------------------------------------------------- +const T = Nullable(Type.String()) // type T = Nullable +``` -const User = Type.Object({ - userId: Type.String({ format: 'uuid' }), - email: Type.String({ format: 'email' }), - online: Type.Boolean(), -}, { additionalProperties: false }) + -//-------------------------------------------------------------------------------------------- -// -// Validate Data -// -//-------------------------------------------------------------------------------------------- +### Recursive Types -const ok = ajv.validate(User, { - userId: '68b4b1d8-0db6-468d-b551-02069a692044', - email: 'dave@domain.com', - online: true -}) // -> ok +Use the Recursive function to create recursive types. + +```typescript +const Node = Type.Recursive(This => Type.Object({ // const Node = { + id: Type.String(), // $id: 'Node', + nodes: Type.Array(This) // type: 'object', +}), { $id: 'Node' }) // properties: { + // id: { + // type: 'string' + // }, + // nodes: { + // type: 'array', + // items: { + // $ref: 'Node' + // } + // } + // }, + // required: [ + // 'id', + // 'nodes' + // ] + // } + +type Node = Static // type Node = { + // id: string + // nodes: Node[] + // } + +function test(node: Node) { + const id = node.nodes[0].nodes[0].id // id is string +} ``` -#### Reference Types + + +### Module Types -Referenced types can be added to AJV with the `ajv.addSchema(...)` function. The following moves the `userId` and `email` property types into a `Type.Namespace(...)` and registers the box with AJV. +Module types are containers for a set of referential types. Modules act as namespaces, enabling types to reference one another via string identifiers. Modules support both singular and mutually recursive references, as well as deferred dereferencing for computed types such as Partial. Types imported from a module are expressed using the Json Schema `$defs` keyword. ```typescript -//-------------------------------------------------------------------------------------------- -// -// Shared Types -// -//-------------------------------------------------------------------------------------------- +const Module = Type.Module({ + PartialUser: Type.Partial(Type.Ref('User')), // TComputed<'Partial', [TRef<'User'>]> + + User: Type.Object({ // TObject<{ + id: Type.String(), // user: TString, + name: Type.String(), // name: TString, + email: Type.String() // email: TString + }), // }> +}) +const User = Module.Import('User') // const User: TImport<{...}, 'User'> + +type User = Static // type User = { + // id: string, + // name: string, + // email: string + // } + +const PartialUser = Module.Import('PartialUser') // const PartialUser: TImport<{...}, 'PartialUser'> + +type PartialUser = Static // type PartialUser = { + // id?: string, + // name?: string, + // email?: string + // } +``` -const Shared = Type.Namespace({ - UserId: Type.String({ format: 'uuid' }), - Email: Type.String({ format: 'email' }) -}, { $id: 'Shared' }) + -//-------------------------------------------------------------------------------------------- -// -// Setup Validator and Register Shared Types -// -//-------------------------------------------------------------------------------------------- +### Template Literal Types -const ajv = addFormats(new Ajv({}), [...]) - .addKeyword('kind') - .addKeyword('modifier') - .addSchema(Shared) // <-- Register Shared Types +TypeBox supports template literal types with the TemplateLiteral function. This type can be created using a syntax similar to the TypeScript template literal syntax or composed from exterior types. TypeBox encodes template literals as regular expressions which enables the template to be checked by Json Schema validators. This type also supports regular expression parsing that enables template patterns to be used for generative types. The following shows both TypeScript and TypeBox usage. -//-------------------------------------------------------------------------------------------- -// -// Create a TypeBox type -// -//-------------------------------------------------------------------------------------------- +```typescript +// TypeScript + +type K = `prop${'A'|'B'|'C'}` // type T = 'propA' | 'propB' | 'propC' + +type R = Record // type R = { + // propA: string + // propB: string + // propC: string + // } + +// TypeBox + +const K = Type.TemplateLiteral('prop${A|B|C}') // const K: TTemplateLiteral<[ + // TLiteral<'prop'>, + // TUnion<[ + // TLiteral<'A'>, + // TLiteral<'B'>, + // TLiteral<'C'>, + // ]> + // ]> + +const R = Type.Record(K, Type.String()) // const R: TObject<{ + // propA: TString, + // propB: TString, + // propC: TString, + // }> +``` -const User = Type.Object({ - userId: Type.Ref(Shared, 'UserId'), - email: Type.Ref(Shared, 'Email'), - online: Type.Boolean() -}, { additionalProperties: false }) + -//-------------------------------------------------------------------------------------------- -// -// Validate Data -// -//-------------------------------------------------------------------------------------------- +### Indexed Access Types -const ok = ajv.validate(User, { - userId: '68b4b1d8-0db6-468d-b551-02069a692044', - email: 'dave@domain.com', - online: true -}) // -> ok +TypeBox supports indexed access types with the Index function. This function enables uniform access to interior property and element types without having to extract them from the underlying schema representation. Index types are supported for Object, Array, Tuple, Union and Intersect types. +```typescript +const T = Type.Object({ // type T = { + x: Type.Number(), // x: number, + y: Type.String(), // y: string, + z: Type.Boolean() // z: boolean +}) // } + +const A = Type.Index(T, ['x']) // type A = T['x'] + // + // ... evaluated as + // + // const A: TNumber + +const B = Type.Index(T, ['x', 'y']) // type B = T['x' | 'y'] + // + // ... evaluated as + // + // const B: TUnion<[ + // TNumber, + // TString, + // ]> + +const C = Type.Index(T, Type.KeyOf(T)) // type C = T[keyof T] + // + // ... evaluated as + // + // const C: TUnion<[ + // TNumber, + // TString, + // TBoolean + // ]> ``` -Please refer to the official AJV [documentation](https://ajv.js.org/guide/getting-started.html) for additional information. + -### OpenAPI +### Mapped Types -TypeBox can be used to create schemas for OpenAPI, however users should be aware of the various differences between the JSON Schema and OpenAPI specifications. Two common instances where OpenAPI diverges from the JSON Schema specification is OpenAPI's handling of `string enum` and `nullable`. The following shows how you can use TypeBox to construct these types. +TypeBox supports mapped types with the Mapped function. This function accepts two arguments, the first is a union type typically derived from KeyOf, the second is a mapping function that receives a mapping key `K` that can be used to index properties of a type. The following implements a mapped type that remaps each property to be `T | null`. ```typescript -import { Type, Static, TNull, TLiteral, TUnion, TSchema } from '@sinclair/typebox' +const T = Type.Object({ // type T = { + x: Type.Number(), // x: number, + y: Type.String(), // y: string, + z: Type.Boolean() // z: boolean +}) // } + +const M = Type.Mapped(Type.KeyOf(T), K => { // type M = { [K in keyof T]: T[K] | null } + return Type.Union([Type.Index(T, K), Type.Null()]) // +}) // ... evaluated as + // + // const M: TObject<{ + // x: TUnion<[TNumber, TNull]>, + // y: TUnion<[TString, TNull]>, + // z: TUnion<[TBoolean, TNull]> + // }> +``` -//-------------------------------------------------------------------------------------------- -// -// Nullable -// -//-------------------------------------------------------------------------------------------- + + +### Conditional Types + +TypeBox supports runtime conditional types with the Extends function. This function performs a structural assignability check against the first (`left`) and second (`right`) arguments and will return either the third (`true`) or fourth (`false`) argument based on the result. The conditional types Exclude and Extract are also supported. The following shows both TypeScript and TypeBox examples of conditional types. + +```typescript +// Extends +const A = Type.Extends( // type A = string extends number ? 1 : 2 + Type.String(), // + Type.Number(), // ... evaluated as + Type.Literal(1), // + Type.Literal(2) // const A: TLiteral<2> +) + +// Extract +const B = Type.Extract( // type B = Extract<1 | 2 | 3, 1> + Type.Union([ // + Type.Literal(1), // ... evaluated as + Type.Literal(2), // + Type.Literal(3) // const B: TLiteral<1> + ]), + Type.Literal(1) +) + +// Exclude +const C = Type.Exclude( // type C = Exclude<1 | 2 | 3, 1> + Type.Union([ // + Type.Literal(1), // ... evaluated as + Type.Literal(2), // + Type.Literal(3) // const C: TUnion<[ + ]), // TLiteral<2>, + Type.Literal(1) // TLiteral<3>, +) // ]> +``` + + + +### Transform Types + +TypeBox supports value decoding and encoding with Transform types. These types work in tandem with the Encode and Decode functions available on the Value and TypeCompiler submodules. Transform types can be used to convert Json encoded values into constructs more natural to JavaScript. The following creates a Transform type to decode numbers into Dates using the Value submodule. + +```typescript +import { Value } from '@sinclair/typebox/value' + +const T = Type.Transform(Type.Number()) + .Decode(value => new Date(value)) // decode: number to Date + .Encode(value => value.getTime()) // encode: Date to number + +const D = Value.Decode(T, 0) // const D = Date(1970-01-01T00:00:00.000Z) +const E = Value.Encode(T, D) // const E = 0 +``` +Use the StaticEncode or StaticDecode types to infer a Transform type. +```typescript +import { Static, StaticDecode, StaticEncode } from '@sinclair/typebox' + +const T = Type.Transform(Type.Array(Type.Number(), { uniqueItems: true })) + .Decode(value => new Set(value)) + .Encode(value => [...value]) + +type D = StaticDecode // type D = Set +type E = StaticEncode // type E = Array +type T = Static // type T = Array +``` + + + +### Unsafe Types + +TypeBox supports user defined types with Unsafe. This type allows you to specify both schema representation and inference type. The following creates an Unsafe type with a number schema that infers as string. + +```typescript +const T = Type.Unsafe({ type: 'number' }) // const T = { type: 'number' } + +type T = Static // type T = string - ? +``` +The Unsafe type is often used to create schematics for extended specifications like OpenAPI. +```typescript + +const Nullable = (schema: T) => Type.Unsafe | null>({ + ...schema, nullable: true +}) + +const T = Nullable(Type.String()) // const T = { + // type: 'string', + // nullable: true + // } + +type T = Static // type T = string | null + +const StringEnum = (values: [...T]) => Type.Unsafe({ + type: 'string', enum: values +}) +const S = StringEnum(['A', 'B', 'C']) // const S = { + // enum: ['A', 'B', 'C'] + // } + +type S = Static // type S = 'A' | 'B' | 'C' +``` + + +### TypeGuard + +TypeBox can check its own types with the TypeGuard module. This module is written for type introspection and provides structural tests for every built-in TypeBox type. Functions of this module return `is` guards which can be used with control flow assertions to obtain schema inference for unknown values. The following guards that the value `T` is TString. + +```typescript +import { TypeGuard, Kind } from '@sinclair/typebox' -function Nullable(schema: T): TUnion<[T, TNull]> { - return { ...schema, nullable: true } as any +const T = { [Kind]: 'String', type: 'string' } + +if(TypeGuard.IsString(T)) { + + // T is TString } +``` -const T = Nullable(Type.String()) // const T = { - // type: 'string', - // nullable: true - // } + -type T = Static // type T = string | null +## Values -//-------------------------------------------------------------------------------------------- +TypeBox provides an optional Value submodule that can be used to perform structural operations on JavaScript values. This submodule includes functionality to create, check and cast values from types as well as check equality, clone, diff and patch JavaScript values. This submodule is provided via optional import. + +```typescript +import { Value } from '@sinclair/typebox/value' +``` + + + +### Assert + +Use the Assert function to assert a value is valid. + +```typescript +let value: unknown = 1 + +Value.Assert(Type.Number(), value) // throws AssertError if invalid +``` + + + +### Create + +Use the Create function to create a value from a type. TypeBox will use default values if specified. + +```typescript +const T = Type.Object({ x: Type.Number(), y: Type.Number({ default: 42 }) }) + +const A = Value.Create(T) // const A = { x: 0, y: 42 } +``` + + + +### Clone + +Use the Clone function to deeply clone a value. + +```typescript +const A = Value.Clone({ x: 1, y: 2, z: 3 }) // const A = { x: 1, y: 2, z: 3 } +``` + + + +### Check + +Use the Check function to type check a value. + +```typescript +const T = Type.Object({ x: Type.Number() }) + +const R = Value.Check(T, { x: 1 }) // const R = true +``` + + + +### Convert + +Use the Convert function to convert a value into its target type if a reasonable conversion is possible. This function may return an invalid value and should be checked before use. Its return type is `unknown`. + +```typescript +const T = Type.Object({ x: Type.Number() }) + +const R1 = Value.Convert(T, { x: '3.14' }) // const R1 = { x: 3.14 } + +const R2 = Value.Convert(T, { x: 'not a number' }) // const R2 = { x: 'not a number' } +``` + + + +### Clean + +Use Clean to remove excess properties from a value. This function does not check the value and returns an unknown type. You should Check the result before use. Clean is a mutable operation. To avoid mutation, Clone the value first. + +```typescript +const T = Type.Object({ + x: Type.Number(), + y: Type.Number() +}) + +const X = Value.Clean(T, null) // const 'X = null + +const Y = Value.Clean(T, { x: 1 }) // const 'Y = { x: 1 } + +const Z = Value.Clean(T, { x: 1, y: 2, z: 3 }) // const 'Z = { x: 1, y: 2 } +``` + + + +### Default + +Use Default to generate missing properties on a value using default schema annotations if available. This function does not check the value and returns an unknown type. You should Check the result before use. Default is a mutable operation. To avoid mutation, Clone the value first. + +```typescript +const T = Type.Object({ + x: Type.Number({ default: 0 }), + y: Type.Number({ default: 0 }) +}) + +const X = Value.Default(T, null) // const 'X = null - non-enumerable + +const Y = Value.Default(T, { }) // const 'Y = { x: 0, y: 0 } + +const Z = Value.Default(T, { x: 1 }) // const 'Z = { x: 1, y: 0 } +``` + + + +### Cast + +Use the Cast function to upcast a value into a target type. This function will retain as much information as possible from the original value. The Cast function is intended to be used in data migration scenarios where existing values need to be upgraded to match a modified type. + +```typescript +const T = Type.Object({ x: Type.Number(), y: Type.Number() }, { additionalProperties: false }) + +const X = Value.Cast(T, null) // const X = { x: 0, y: 0 } + +const Y = Value.Cast(T, { x: 1 }) // const Y = { x: 1, y: 0 } + +const Z = Value.Cast(T, { x: 1, y: 2, z: 3 }) // const Z = { x: 1, y: 2 } +``` + + + +### Decode + +Use the Decode function to decode a value from a type or throw if the value is invalid. The return value will infer as the decoded type. This function will run Transform codecs if available. + +```typescript +const A = Value.Decode(Type.String(), 'hello') // const A = 'hello' + +const B = Value.Decode(Type.String(), 42) // throw +``` + + +### Encode + +Use the Encode function to encode a value to a type or throw if the value is invalid. The return value will infer as the encoded type. This function will run Transform codecs if available. + +```typescript +const A = Value.Encode(Type.String(), 'hello') // const A = 'hello' + +const B = Value.Encode(Type.String(), 42) // throw +``` + + + +### Parse + +Use the Parse function to parse a value. This function calls the `Clone` `Clean`, `Default`, `Convert`, `Assert` and `Decode` Value functions in this exact order to process a value. + +```typescript +const R = Value.Parse(Type.String(), 'hello') // const R: string = "hello" + +const E = Value.Parse(Type.String(), undefined) // throws AssertError +``` + +You can override the order in which functions are run, or omit functions entirely using the following. + +```typescript +// Runs no functions. + +const R = Value.Parse([], Type.String(), 12345) + +// Runs the Assert() function. + +const E = Value.Parse(['Assert'], Type.String(), 12345) + +// Runs the Convert() function followed by the Assert() function. + +const S = Value.Parse(['Convert', 'Assert'], Type.String(), 12345) +``` + + + +### Equal + +Use the Equal function to deeply check for value equality. + +```typescript +const R = Value.Equal( // const R = true + { x: 1, y: 2, z: 3 }, + { x: 1, y: 2, z: 3 } +) +``` + + + +### Hash + +Use the Hash function to create a [FNV1A-64](https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function) non-cryptographic hash of a value. + +```typescript +const A = Value.Hash({ x: 1, y: 2, z: 3 }) // const A = 2910466848807138541n + +const B = Value.Hash({ x: 1, y: 4, z: 3 }) // const B = 1418369778807423581n +``` + + + +### Diff + +Use the Diff function to generate a sequence of edits that will transform one value into another. + +```typescript +const E = Value.Diff( // const E = [ + { x: 1, y: 2, z: 3 }, // { type: 'update', path: '/y', value: 4 }, + { y: 4, z: 5, w: 6 } // { type: 'update', path: '/z', value: 5 }, +) // { type: 'insert', path: '/w', value: 6 }, + // { type: 'delete', path: '/x' } + // ] +``` + + + +### Patch + +Use the Patch function to apply a sequence of edits. + +```typescript +const A = { x: 1, y: 2 } + +const B = { x: 3 } + +const E = Value.Diff(A, B) // const E = [ + // { type: 'update', path: '/x', value: 3 }, + // { type: 'delete', path: '/y' } + // ] + +const C = Value.Patch(A, E) // const C = { x: 3 } +``` + + + +### Errors + +Use the Errors function to enumerate validation errors. + +```typescript +const T = Type.Object({ x: Type.Number(), y: Type.Number() }) + +const R = [...Value.Errors(T, { x: '42' })] // const R = [{ + // schema: { type: 'number' }, + // path: '/x', + // value: '42', + // message: 'Expected number' + // }, { + // schema: { type: 'number' }, + // path: '/y', + // value: undefined, + // message: 'Expected number' + // }] +``` + + + +### Mutate + +Use the Mutate function to perform a deep mutable value assignment while retaining internal references. + +```typescript +const Y = { z: 1 } // const Y = { z: 1 } +const X = { y: Y } // const X = { y: { z: 1 } } +const A = { x: X } // const A = { x: { y: { z: 1 } } } + +Value.Mutate(A, { x: { y: { z: 2 } } }) // A' = { x: { y: { z: 2 } } } + +const R0 = A.x.y.z === 2 // const R0 = true +const R1 = A.x.y === Y // const R1 = true +const R2 = A.x === X // const R2 = true +``` + + + +### Pointer + +Use ValuePointer to perform mutable updates on existing values using [RFC6901](https://www.rfc-editor.org/rfc/rfc6901) Json Pointers. + +```typescript +import { ValuePointer } from '@sinclair/typebox/value' + +const A = { x: 0, y: 0, z: 0 } + +ValuePointer.Set(A, '/x', 1) // A' = { x: 1, y: 0, z: 0 } +ValuePointer.Set(A, '/y', 1) // A' = { x: 1, y: 1, z: 0 } +ValuePointer.Set(A, '/z', 1) // A' = { x: 1, y: 1, z: 1 } +``` + + + + + +## Syntax Types + +TypeBox provides experimental support for parsing TypeScript annotation syntax into TypeBox types. + +This feature is provided via optional import. + +```typescript +import { Syntax } from '@sinclair/typebox/syntax' +``` + + + +### Create + +Use the Syntax function to create TypeBox types from TypeScript syntax ([Example](https://www.typescriptlang.org/play/?moduleResolution=99&module=199&ts=5.8.0-beta#code/JYWwDg9gTgLgBAbzgZQJ4DsYEMAecC+cAZlBCHAOQACAzsOgMYA2WwUA9DKmAKYBGEHOxoZsOCgChQkWIhTYYwBgWKly1OoxZtO3foMkSGEdDXgAVOAF4Uo3AAoABkhwAuOOgCuIPjygAaOFR3Lx8-AkcASjgY2Jj2djhjUwt3cwB5PgArHgYYAB4ECTiS0rLyisrYhNi3OHMAOW9fAOKq9o7OuBqY4PqmsKg2rpHR+MT8AD4JCS5eeut5LEUGfLmeCCJ6ybHKmvWFmyLdk86euDrQlv9h07uy876rv1v7t-GCIA)) + +```typescript +const T = Syntax(`{ x: number, y: number }`) // const T: TObject<{ + // x: TNumber, + // y: TNumber + // }> + +type T = Static // type T = { + // x: number, + // y: number + // } +``` + + + +### Parameters + +Syntax types can be parameterized to receive exterior types ([Example](https://www.typescriptlang.org/play/?moduleResolution=99&module=199&ts=5.8.0-beta#code/JYWwDg9gTgLgBAbzgZQJ4DsYEMAecC+cAZlBCHAOQACAzsOgMYA2WwUA9DKmAKYBGEHOxoZsOCgCgJDCOhrwAKnAC8KUbgAUAAyQ4AXHHQBXEHx5QANHFQHjp8wS0BKOK7ev27ODLmKDCgHk+ACseBhgAHgQJd1i4+ITEpLdPN304BQA5EzNLGOSCwqK4VNcbDOz7KHzi2rqPL3wAPikfeRQVNUxNJCV8Ky0ABSxYYCwmCIUm52LUtvhkfyDQ8Kia+o2C0rh0wLAYYFlxycrcpot1zav47fK9g6OJrJzzFuv3m8amoA)) + +```typescript +const T = Syntax(`{ x: number, y: number }`) // const T: TObject<{ + // x: TNumber, + // y: TNumber + // }> + +const S = Syntax({ T }, `Partial`) // const S: TObject<{ + // x: TOptional, + // y: TOptional + // }> +``` + + + + + +### Generics + +Syntax types support generic parameters in the following way ([Example](https://www.typescriptlang.org/play/?moduleResolution=99&module=199&ts=5.8.0-beta#code/JYWwDg9gTgLgBAbzgZQJ4DsYEMAecC+cAZlBCHAOQACAzsOgMYA2WwUA9DKmAKYBGEHOxoZsOCgChQkWIhTYYwBgWKly1OoxZtO3foMkSGEdDXgA1HgxjQ4AXhSjcACgAGAHgAaAGjgBNXwAtAD45CTg4HAAuOB84cLhUGID4iIAvGMD4-FcASgkjEzM4ACEsOhpLa2gae0dMFyQqmygCX1cEBOi4Zuh3AEZfAAZh4O8EpJ6rFvcRuEG4IbGEjKnqqFnh337lnPyJLl5S8uBK6Zq65AUld0OeCCJjit6oGlCIiPZ2ODun05fag5Oh8QaCweCIZCoV8Pt0kN0FpM5qshm0ElCMZisSCYRFJvCYnNJgsUWjseSKeDcXBVgTFr4kb5Vv0COjKezsTD8EA)) + +```typescript +const Vector = Syntax(` { + x: X, + y: Y, + z: Z +}`) + +const BasisVectors = Syntax({ Vector }, `{ + x: Vector<1, 0, 0>, + y: Vector<0, 1, 0>, + z: Vector<0, 0, 1>, +}`) + +type BasisVectors = Static // type BasisVectors = { + // x: { x: 1, y: 0, z: 0 }, + // y: { x: 0, y: 1, z: 0 }, + // z: { x: 0, y: 0, z: 1 } + // } +``` + + + +### Options + +Options can be passed via the last parameter. + +```typescript +const T = Syntax(`number`, { minimum: 42 }) // const T = { + // type: 'number', + // minimum: 42 + // } +``` + + + +### NoInfer + +Syntax parsing is an expensive type level operation and can impact on language service performance. Use the NoInfer function parse syntax at runtime only. + +```typescript +import { NoInfer } from '@sinclair/typebox/syntax' + +const T = NoInfer(`number | string`) // const T: TSchema = { + // anyOf: [ + // { type: 'number' }, + // { type: 'string' } + // ] + // } +``` + + + +## TypeRegistry + +The TypeBox type system can be extended with additional types and formats using the TypeRegistry and FormatRegistry modules. These modules integrate deeply with TypeBox's internal type checking infrastructure and can be used to create application specific types, or register schematics for alternative specifications. + + + +### TypeRegistry + +Use the TypeRegistry to register a type. The Kind must match the registered type name. + +```typescript +import { TSchema, Kind, TypeRegistry } from '@sinclair/typebox' + +TypeRegistry.Set('Foo', (schema, value) => value === 'foo') + +const Foo = { [Kind]: 'Foo' } as TSchema + +const A = Value.Check(Foo, 'foo') // const A = true + +const B = Value.Check(Foo, 'bar') // const B = false +``` + + + +### FormatRegistry + +Use the FormatRegistry to register a string format. + +```typescript +import { FormatRegistry } from '@sinclair/typebox' + +FormatRegistry.Set('foo', (value) => value === 'foo') + +const T = Type.String({ format: 'foo' }) + +const A = Value.Check(T, 'foo') // const A = true + +const B = Value.Check(T, 'bar') // const B = false +``` + + + +## TypeCheck + +TypeBox types target Json Schema Draft 7 and are compatible with any validator that supports this specification. TypeBox also provides a built-in type checking compiler designed specifically for TypeBox types that offers high performance compilation and value checking. + +The following sections detail using Ajv and the TypeBox compiler infrastructure. + + + +## Ajv + +The following shows the recommended setup for Ajv. + +```bash +$ npm install ajv ajv-formats --save +``` + +```typescript +import { Type } from '@sinclair/typebox' +import addFormats from 'ajv-formats' +import Ajv from 'ajv' + +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' +]) + +const validate = ajv.compile(Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number() +})) + +const R = validate({ x: 1, y: 2, z: 3 }) // const R = true +``` + + + +### TypeCompiler + +The TypeBox TypeCompiler is a high performance JIT validation compiler that transforms TypeBox types into optimized JavaScript validation routines. The compiler is tuned for fast compilation as well as fast value assertion. It is built to serve as a validation backend that can be integrated into larger applications. It can also be used for code generation. + +The TypeCompiler is provided as an optional import. + +```typescript +import { TypeCompiler } from '@sinclair/typebox/compiler' +``` + +Use the Compile function to JIT compile a type. Note that compilation is generally an expensive operation and should only be performed once per type during application start up. TypeBox does not cache previously compiled types, and applications are expected to hold references to each compiled type for the lifetime of the application. + +```typescript +const C = TypeCompiler.Compile(Type.Object({ // const C: TypeCheck> + +const R = C.Check({ x: 1, y: 2, z: 3 }) // const R = true +``` + +Use the Errors function to generate diagnostic errors for a value. The Errors function will return an iterator that when enumerated; will perform an exhaustive check across the entire value yielding any error found. For performance, this function should only be called after a failed Check. Applications may also choose to yield only the first value to avoid exhaustive error generation. + +```typescript +const C = TypeCompiler.Compile(Type.Object({ // const C: TypeCheck> + +const value = { } + +const first = C.Errors(value).First() // const first = { + // schema: { type: 'number' }, + // path: '/x', + // value: undefined, + // message: 'Expected number' + // } + +const all = [...C.Errors(value)] // const all = [{ + // schema: { type: 'number' }, + // path: '/x', + // value: undefined, + // message: 'Expected number' + // }, { + // schema: { type: 'number' }, + // path: '/y', + // value: undefined, + // message: 'Expected number' + // }, { + // schema: { type: 'number' }, + // path: '/z', + // value: undefined, + // message: 'Expected number' + // }] +``` + +Use the Code function to generate assertion functions as strings. This function can be used to generate code that can be written to disk as importable modules. This technique is sometimes referred to as Ahead of Time (AOT) compilation. The following generates code to check a string. + +```typescript +const C = TypeCompiler.Code(Type.String()) // const C = `return function check(value) { + // return ( + // (typeof value === 'string') + // ) + // }` +``` + + + +## TypeMap + +TypeBox offers an external package for bidirectional mapping between TypeBox, Valibot, and Zod type libraries. It also includes syntax parsing support for Valibot and Zod and supports the Standard Schema specification. For more details on TypeMap, refer to the project repository. + +[TypeMap Repository](https://github.com/sinclairzx81/typemap) + + + +### Usage + +TypeMap needs to be installed separately + +```bash +$ npm install @sinclair/typemap +``` + +Once installed it offers advanced structural remapping between various runtime type libraries ([Example](https://www.typescriptlang.org/play/?moduleResolution=99&module=199&ts=5.8.0-beta#code/JYWwDg9gTgLgBAbzgFQJ5gKYCEIA8A0cAyqgHYwCGBcAWhACZwC+cAZlBCHAOQACAzsFIBjADYVgUAPQx0GEBTDcAUMuERS-eMjgBeFHJy4AFAAMkuAFxxSAVxAAjDFEKprdx88IAvd-adQzKYAlHBwUlJw6pra1sgA8g4AVhjCMAA8CMphObl5+QWFRcW5ETlWKABy-s4A3NkljU3NBWVhblU1UPUtvX3FbXC+nZ7dDf0TE2VMAHyq0VrEesRklCbIoS1lC-BE1twWfqOuRwE+p87MKmoaiwBKy3T0xkTBAHRgFFD8GMZ2oqJNnltrd4HdrFlJltImEKh4Aj0oU1Bh14XVxkiBjChhcxpjGtMwkA)) + +```typescript +import { TypeBox, Syntax, Zod } from '@sinclair/typemap' + +const T = TypeBox(`{ x: number, y: number, z: number }`) // const T: TObject<{ + // x: TNumber; + // y: TNumber; + // z: TNumber; + // }> + +const S = Syntax(T) // const S: '{ x: number, y: number, z: number }' + +const R = Zod(S).parse(null) // const R: { + // x: number; + // y: number; + // z: number; + // } +``` + + + +## TypeSystem + +The TypeBox TypeSystem module provides configurations to use either Json Schema or TypeScript type checking semantics. Configurations made to the TypeSystem module are observed by the TypeCompiler, Value and Error modules. + + + +### Policies + +TypeBox validates using standard Json Schema assertion policies by default. The TypeSystemPolicy module can override some of these to have TypeBox assert values inline with TypeScript static checks. It also provides overrides for certain checking rules related to non-serializable values (such as void) which can be helpful in Json based protocols such as Json Rpc 2.0. + +The following overrides are available. + +```typescript +import { TypeSystemPolicy } from '@sinclair/typebox/system' + +// Disallow undefined values for optional properties (default is false) // -// StringUnion<[...]> +// const A: { x?: number } = { x: undefined } - disallowed when enabled + +TypeSystemPolicy.ExactOptionalPropertyTypes = true + +// Allow arrays to validate as object types (default is false) // -//-------------------------------------------------------------------------------------------- +// const A: {} = [] - allowed in TS -type IntoStringUnion = {[K in keyof T]: T[K] extends string ? TLiteral: never } +TypeSystemPolicy.AllowArrayObject = true -function StringUnion(values: [...T]): TUnion> { - return { enum: values } as any -} +// Allow numeric values to be NaN or + or - Infinity (default is false) +// +// const A: number = NaN - allowed in TS + +TypeSystemPolicy.AllowNaN = true -const T = StringUnion(['A', 'B', 'C']) // const T = { - // enum: ['A', 'B', 'C'] - // } +// Allow void types to check with undefined and null (default is false) +// +// Used to signal void return on Json-Rpc 2.0 protocol + +TypeSystemPolicy.AllowNullVoid = true +``` + + + +## Error Function + +Error messages in TypeBox can be customized by defining an ErrorFunction. This function allows for the localization of error messages as well as enabling custom error messages for custom types. By default, TypeBox will generate messages using the `en-US` locale. To support additional locales, you can replicate the function found in `src/errors/function.ts` and create a locale specific translation. The function can then be set via SetErrorFunction. -type T = Static // type T = 'A' | 'B' | 'C' +The following example shows an inline error function that intercepts errors for String, Number and Boolean only. The DefaultErrorFunction is used to return a default error message. + + +```typescript +import { SetErrorFunction, DefaultErrorFunction, ValueErrorType } from '@sinclair/typebox/errors' + +SetErrorFunction((error) => { // i18n override + switch(error.errorType) { + /* en-US */ case ValueErrorType.String: return 'Expected string' + /* fr-FR */ case ValueErrorType.Number: return 'Nombre attendu' + /* ko-KR */ case ValueErrorType.Boolean: return '예상 부울' + /* en-US */ default: return DefaultErrorFunction(error) + } +}) +const T = Type.Object({ // const T: TObject<{ + x: Type.String(), // TString, + y: Type.Number(), // TNumber, + z: Type.Boolean() // TBoolean +}) // }> + +const E = [...Value.Errors(T, { // const E = [{ + x: null, // type: 48, + y: null, // schema: { ... }, + z: null // path: '/x', +})] // value: null, + // message: 'Expected string' + // }, { + // type: 34, + // schema: { ... }, + // path: '/y', + // value: null, + // message: 'Nombre attendu' + // }, { + // type: 14, + // schema: { ... }, + // path: '/z', + // value: null, + // message: '예상 부울' + // }] ``` + + + +## TypeBox Workbench + +TypeBox offers a web based code generation tool that can convert TypeScript types into TypeBox types as well as several other ecosystem libraries. + +[TypeBox Workbench Link Here](https://sinclairzx81.github.io/typebox-workbench/) + + + +## TypeBox Codegen + +TypeBox provides a code generation library that can be integrated into toolchains to automate type translation between TypeScript and TypeBox. This library also includes functionality to transform TypeScript types to other ecosystem libraries. + +[TypeBox Codegen Link Here](https://github.com/sinclairzx81/typebox-codegen) + + + +## Ecosystem + +The following is a list of community packages that offer general tooling, extended functionality and framework integration support for TypeBox. + +| Package | Description | +| ------------- | ------------- | +| [drizzle-typebox](https://www.npmjs.com/package/drizzle-typebox) | Generates TypeBox types from Drizzle ORM schemas | +| [elysia](https://github.com/elysiajs/elysia) | Fast and friendly Bun web framework | +| [fastify-type-provider-typebox](https://github.com/fastify/fastify-type-provider-typebox) | Fastify TypeBox integration with the Fastify Type Provider | +| [feathersjs](https://github.com/feathersjs/feathers) | The API and real-time application framework | +| [fetch-typebox](https://github.com/erfanium/fetch-typebox) | Drop-in replacement for fetch that brings easy integration with TypeBox | +| [@lonli-lokli/fetcher-typebox](https://github.com/Lonli-Lokli/fetcher-ts/tree/master/packages/fetcher-typebox) | A strongly-typed fetch wrapper for TypeScript applications with optional runtime validation using TypeBox | +| [h3-typebox](https://github.com/kevinmarrec/h3-typebox) | Schema validation utilities for h3 using TypeBox & Ajv | +| [http-wizard](https://github.com/flodlc/http-wizard) | Type safe http client library for Fastify | +| [json2typebox](https://github.com/hacxy/json2typebox) | Creating TypeBox code from Json Data | +| [nominal-typebox](https://github.com/Coder-Spirit/nominal/tree/main/%40coderspirit/nominal-typebox) | Allows devs to integrate nominal types into TypeBox schemas | +| [openapi-box](https://github.com/geut/openapi-box) | Generate TypeBox types from OpenApi IDL + Http client library | +| [prismabox](https://github.com/m1212e/prismabox) | Converts a prisma.schema to TypeBox schema matching the database models | +| [schema2typebox](https://github.com/xddq/schema2typebox) | Creating TypeBox code from Json Schemas | +| [sveltekit-superforms](https://github.com/ciscoheat/sveltekit-superforms) | A comprehensive SvelteKit form library for server and client validation | +| [ts2typebox](https://github.com/xddq/ts2typebox) | Creating TypeBox code from Typescript types | +| [typebox-cli](https://github.com/gsuess/typebox-cli) | Generate Schema with TypeBox from the CLI | +| [typebox-form-parser](https://github.com/jtlapp/typebox-form-parser) | Parses form and query data based on TypeBox schemas | +| [typebox-schema-faker](https://github.com/iam-medvedev/typebox-schema-faker) | Generate fake data from TypeBox schemas for testing, prototyping and development | + + + + +## Benchmark + +This project maintains a set of benchmarks that measure Ajv, Value and TypeCompiler compilation and validation performance. These benchmarks can be run locally by cloning this repository and running `npm run benchmark`. The results below show for Ajv version 8.12.0 running on Node 20.10.0. + +For additional comparative benchmarks, please refer to [typescript-runtime-type-benchmarks](https://moltar.github.io/typescript-runtime-type-benchmarks/). + + + +### Compile + +This benchmark measures compilation performance for varying types. + +```typescript +┌────────────────────────────┬────────────┬──────────────┬──────────────┬──────────────┐ +│ (index) │ Iterations │ Ajv │ TypeCompiler │ Performance │ +├────────────────────────────┼────────────┼──────────────┼──────────────┼──────────────┤ +│ Literal_String │ 1000 │ ' 211 ms' │ ' 8 ms' │ ' 26.38 x' │ +│ Literal_Number │ 1000 │ ' 185 ms' │ ' 5 ms' │ ' 37.00 x' │ +│ Literal_Boolean │ 1000 │ ' 195 ms' │ ' 4 ms' │ ' 48.75 x' │ +│ Primitive_Number │ 1000 │ ' 149 ms' │ ' 7 ms' │ ' 21.29 x' │ +│ Primitive_String │ 1000 │ ' 135 ms' │ ' 5 ms' │ ' 27.00 x' │ +│ Primitive_String_Pattern │ 1000 │ ' 193 ms' │ ' 10 ms' │ ' 19.30 x' │ +│ Primitive_Boolean │ 1000 │ ' 152 ms' │ ' 4 ms' │ ' 38.00 x' │ +│ Primitive_Null │ 1000 │ ' 147 ms' │ ' 4 ms' │ ' 36.75 x' │ +│ Object_Unconstrained │ 1000 │ ' 1065 ms' │ ' 26 ms' │ ' 40.96 x' │ +│ Object_Constrained │ 1000 │ ' 1183 ms' │ ' 26 ms' │ ' 45.50 x' │ +│ Object_Vector3 │ 1000 │ ' 407 ms' │ ' 9 ms' │ ' 45.22 x' │ +│ Object_Box3D │ 1000 │ ' 1777 ms' │ ' 24 ms' │ ' 74.04 x' │ +│ Tuple_Primitive │ 1000 │ ' 485 ms' │ ' 11 ms' │ ' 44.09 x' │ +│ Tuple_Object │ 1000 │ ' 1344 ms' │ ' 17 ms' │ ' 79.06 x' │ +│ Composite_Intersect │ 1000 │ ' 606 ms' │ ' 14 ms' │ ' 43.29 x' │ +│ Composite_Union │ 1000 │ ' 522 ms' │ ' 17 ms' │ ' 30.71 x' │ +│ Math_Vector4 │ 1000 │ ' 851 ms' │ ' 9 ms' │ ' 94.56 x' │ +│ Math_Matrix4 │ 1000 │ ' 406 ms' │ ' 10 ms' │ ' 40.60 x' │ +│ Array_Primitive_Number │ 1000 │ ' 367 ms' │ ' 6 ms' │ ' 61.17 x' │ +│ Array_Primitive_String │ 1000 │ ' 339 ms' │ ' 7 ms' │ ' 48.43 x' │ +│ Array_Primitive_Boolean │ 1000 │ ' 325 ms' │ ' 5 ms' │ ' 65.00 x' │ +│ Array_Object_Unconstrained │ 1000 │ ' 1863 ms' │ ' 21 ms' │ ' 88.71 x' │ +│ Array_Object_Constrained │ 1000 │ ' 1535 ms' │ ' 18 ms' │ ' 85.28 x' │ +│ Array_Tuple_Primitive │ 1000 │ ' 829 ms' │ ' 14 ms' │ ' 59.21 x' │ +│ Array_Tuple_Object │ 1000 │ ' 1674 ms' │ ' 14 ms' │ ' 119.57 x' │ +│ Array_Composite_Intersect │ 1000 │ ' 789 ms' │ ' 13 ms' │ ' 60.69 x' │ +│ Array_Composite_Union │ 1000 │ ' 822 ms' │ ' 15 ms' │ ' 54.80 x' │ +│ Array_Math_Vector4 │ 1000 │ ' 1129 ms' │ ' 14 ms' │ ' 80.64 x' │ +│ Array_Math_Matrix4 │ 1000 │ ' 673 ms' │ ' 9 ms' │ ' 74.78 x' │ +└────────────────────────────┴────────────┴──────────────┴──────────────┴──────────────┘ +``` + + + +### Validate + +This benchmark measures validation performance for varying types. + +```typescript +┌────────────────────────────┬────────────┬──────────────┬──────────────┬──────────────┬──────────────┐ +│ (index) │ Iterations │ ValueCheck │ Ajv │ TypeCompiler │ Performance │ +├────────────────────────────┼────────────┼──────────────┼──────────────┼──────────────┼──────────────┤ +│ Literal_String │ 1000000 │ ' 17 ms' │ ' 5 ms' │ ' 5 ms' │ ' 1.00 x' │ +│ Literal_Number │ 1000000 │ ' 14 ms' │ ' 18 ms' │ ' 9 ms' │ ' 2.00 x' │ +│ Literal_Boolean │ 1000000 │ ' 14 ms' │ ' 20 ms' │ ' 9 ms' │ ' 2.22 x' │ +│ Primitive_Number │ 1000000 │ ' 17 ms' │ ' 19 ms' │ ' 9 ms' │ ' 2.11 x' │ +│ Primitive_String │ 1000000 │ ' 17 ms' │ ' 18 ms' │ ' 10 ms' │ ' 1.80 x' │ +│ Primitive_String_Pattern │ 1000000 │ ' 172 ms' │ ' 46 ms' │ ' 41 ms' │ ' 1.12 x' │ +│ Primitive_Boolean │ 1000000 │ ' 14 ms' │ ' 19 ms' │ ' 10 ms' │ ' 1.90 x' │ +│ Primitive_Null │ 1000000 │ ' 16 ms' │ ' 19 ms' │ ' 9 ms' │ ' 2.11 x' │ +│ Object_Unconstrained │ 1000000 │ ' 437 ms' │ ' 28 ms' │ ' 14 ms' │ ' 2.00 x' │ +│ Object_Constrained │ 1000000 │ ' 653 ms' │ ' 46 ms' │ ' 37 ms' │ ' 1.24 x' │ +│ Object_Vector3 │ 1000000 │ ' 201 ms' │ ' 22 ms' │ ' 12 ms' │ ' 1.83 x' │ +│ Object_Box3D │ 1000000 │ ' 961 ms' │ ' 37 ms' │ ' 19 ms' │ ' 1.95 x' │ +│ Object_Recursive │ 1000000 │ ' 3715 ms' │ ' 363 ms' │ ' 174 ms' │ ' 2.09 x' │ +│ Tuple_Primitive │ 1000000 │ ' 107 ms' │ ' 23 ms' │ ' 11 ms' │ ' 2.09 x' │ +│ Tuple_Object │ 1000000 │ ' 375 ms' │ ' 28 ms' │ ' 15 ms' │ ' 1.87 x' │ +│ Composite_Intersect │ 1000000 │ ' 377 ms' │ ' 22 ms' │ ' 12 ms' │ ' 1.83 x' │ +│ Composite_Union │ 1000000 │ ' 337 ms' │ ' 30 ms' │ ' 17 ms' │ ' 1.76 x' │ +│ Math_Vector4 │ 1000000 │ ' 137 ms' │ ' 23 ms' │ ' 11 ms' │ ' 2.09 x' │ +│ Math_Matrix4 │ 1000000 │ ' 576 ms' │ ' 37 ms' │ ' 28 ms' │ ' 1.32 x' │ +│ Array_Primitive_Number │ 1000000 │ ' 145 ms' │ ' 23 ms' │ ' 12 ms' │ ' 1.92 x' │ +│ Array_Primitive_String │ 1000000 │ ' 152 ms' │ ' 22 ms' │ ' 13 ms' │ ' 1.69 x' │ +│ Array_Primitive_Boolean │ 1000000 │ ' 131 ms' │ ' 20 ms' │ ' 13 ms' │ ' 1.54 x' │ +│ Array_Object_Unconstrained │ 1000000 │ ' 2821 ms' │ ' 62 ms' │ ' 45 ms' │ ' 1.38 x' │ +│ Array_Object_Constrained │ 1000000 │ ' 2958 ms' │ ' 119 ms' │ ' 134 ms' │ ' 0.89 x' │ +│ Array_Object_Recursive │ 1000000 │ ' 14695 ms' │ ' 1621 ms' │ ' 635 ms' │ ' 2.55 x' │ +│ Array_Tuple_Primitive │ 1000000 │ ' 478 ms' │ ' 35 ms' │ ' 28 ms' │ ' 1.25 x' │ +│ Array_Tuple_Object │ 1000000 │ ' 1623 ms' │ ' 63 ms' │ ' 48 ms' │ ' 1.31 x' │ +│ Array_Composite_Intersect │ 1000000 │ ' 1582 ms' │ ' 43 ms' │ ' 30 ms' │ ' 1.43 x' │ +│ Array_Composite_Union │ 1000000 │ ' 1331 ms' │ ' 76 ms' │ ' 40 ms' │ ' 1.90 x' │ +│ Array_Math_Vector4 │ 1000000 │ ' 564 ms' │ ' 38 ms' │ ' 24 ms' │ ' 1.58 x' │ +│ Array_Math_Matrix4 │ 1000000 │ ' 2382 ms' │ ' 111 ms' │ ' 83 ms' │ ' 1.34 x' │ +└────────────────────────────┴────────────┴──────────────┴──────────────┴──────────────┴──────────────┘ +``` + + + +### Compression + +The following table lists esbuild compiled and minified sizes for each TypeBox module. + +```typescript +┌──────────────────────┬────────────┬────────────┬─────────────┐ +│ (index) │ Compiled │ Minified │ Compression │ +├──────────────────────┼────────────┼────────────┼─────────────┤ +│ typebox/compiler │ '122.4 kb' │ ' 53.4 kb' │ '2.29 x' │ +│ typebox/errors │ ' 67.6 kb' │ ' 29.6 kb' │ '2.28 x' │ +│ typebox/syntax │ '132.9 kb' │ ' 54.2 kb' │ '2.45 x' │ +│ typebox/system │ ' 7.4 kb' │ ' 3.2 kb' │ '2.33 x' │ +│ typebox/value │ '150.1 kb' │ ' 62.2 kb' │ '2.41 x' │ +│ typebox │ '106.8 kb' │ ' 43.2 kb' │ '2.47 x' │ +└──────────────────────┴────────────┴────────────┴─────────────┘ +``` + + + +## Contribute + +TypeBox is open to community contribution. Please ensure you submit an open issue before submitting your pull request. The TypeBox project prefers open community discussion before accepting new features. diff --git a/spec/schema/any.ts b/spec/schema/any.ts deleted file mode 100644 index decdc2667..000000000 --- a/spec/schema/any.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Type } from '@sinclair/typebox' -import { ok, fail } from './validate' - -describe("Any", () => { - 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 deleted file mode 100644 index 6c4fcbded..000000000 --- a/spec/schema/array.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { Type } from '@sinclair/typebox' -import { ok, fail } from './validate' - -describe("Array", () => { - it('Should validate an array of any', () => { - const T = Type.Array(Type.Any()) - ok(T, [0, true, 'hello', {}]) - }) - - it('Should not validate varying array when item is number', () => { - const T = Type.Array(Type.Number()) - fail(T, [1, 2, 3, 'hello']) - }) - - 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('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('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 deleted file mode 100644 index 6ce216837..000000000 --- a/spec/schema/boolean.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Type } from '@sinclair/typebox' -import { ok, fail } from './validate' - -describe("Boolean", () => { - - 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 deleted file mode 100644 index a605d00ba..000000000 --- a/spec/schema/box.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { Type } from '@sinclair/typebox' -import { ok, fail } from './validate' - -describe("Box", () => { - - 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() }) - const Math3D = Type.Namespace({ Vector2, Vector3, Vector4 }, { $id: 'Math3D' }) - const Vertex = Type.Object({ - position: Type.Ref(Math3D, 'Vector4'), - normal: Type.Ref(Math3D, 'Vector3'), - uv: Type.Ref(Math3D, 'Vector2'), - }) - - ok(Vertex, { - position: { x: 1, y: 1, z: 1, w: 1 }, - normal: { x: 1, y: 1, z: 1 }, - uv: { x: 1, y: 1 }, - }, [Math3D]) - }) - - 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.Namespace({ Vector2, Vector3, Vector4 }, { $id: 'Math3D' }) - const Vertex = Type.Object({ - position: Type.Ref(Math3D, 'Vector4'), - normal: Type.Ref(Math3D, 'Vector3'), - uv: Type.Ref(Math3D, 'Vector2'), - }) - - fail(Vertex, { - position: { x: 1, y: 1, z: 1, w: 1 }, - normal: { x: 1, y: 1, z: 1 }, - }, [Math3D]) - }) - - 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.Namespace({ Vector2, Vector3, Vector4 }, { $id: 'Math3D' }) - const Vertex = Type.Object({ - position: Type.Ref(Math3D, 'Vector4'), - normal: Type.Ref(Math3D, 'Vector3'), - uv: Type.Ref(Math3D, 'Vector2'), - }) - fail(Vertex, { - position: { x: 1, y: 1, z: 1, w: 1 }, - normal: { x: 1, y: 1, z: 1 }, - uv: { x: 1, y: 'not a number'}, - }, [Math3D]) - }) - - 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.Namespace({ Vector2, Vector3, Vector4 }, { $id: 'Math3D' }) - const Vertex = Type.Object({ - position: Type.Ref(Math3D, 'Vector4'), - normal: Type.Ref(Math3D, 'Vector3'), - uv: Type.Ref(Math3D, 'Vector2'), - }) - fail(Vertex, { - position: { x: 1, y: 1, z: 1, w: 1 }, - normal: { x: 1, y: 1, z: 1 }, - uv: { x: 1, y: 1}, - }, []) - }) -}) diff --git a/spec/schema/enum.ts b/spec/schema/enum.ts deleted file mode 100644 index 2a6356c3c..000000000 --- a/spec/schema/enum.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Type } from '@sinclair/typebox' -import { ok, fail } from './validate' - -describe('Enum', () => { - - 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) - }) - 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') - }) - - 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') - }) - - 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) - }) - - 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) - }) -}) diff --git a/spec/schema/index.ts b/spec/schema/index.ts deleted file mode 100644 index a4a273c52..000000000 --- a/spec/schema/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -import './any' -import './array' -import './boolean' -import './box' -import './enum' -import './intersect' -import './keyof' -import './literal' -import './modifier' -import './null' -import './number' -import './object' -import './omit' -import './optional' -import './partial' -import './pick' -import './readonly-optional' -import './readonly' -import './rec' -import './record' -import './ref' -import './regex' -import './required' -import './strict' -import './string' -import './tuple' -import './union' -import './unknown' \ No newline at end of file diff --git a/spec/schema/intersect.ts b/spec/schema/intersect.ts deleted file mode 100644 index 4a7f9cd5d..000000000 --- a/spec/schema/intersect.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { Type } from '@sinclair/typebox' -import { ok, fail } from './validate' - -describe('Intersect', () => { - - 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 }) - }) - - 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 }) - }) - - 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 - }) - }) - - describe('Should intersect with partial', () => { - const A = Type.Object({ a: Type.Number() }) - const B = Type.Object({ b: Type.Number() }) - const P = Type.Intersect([Type.Partial(A), Type.Partial(B)], { unevaluatedProperties: false }) - ok(P, { a: 1, b: 2 }) - ok(P, { a: 1 }) - ok(P, { b: 1 }) - fail(P, { c: 1 }) - }) -}) diff --git a/spec/schema/keyof.ts b/spec/schema/keyof.ts deleted file mode 100644 index baa1ec940..000000000 --- a/spec/schema/keyof.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Type } from '@sinclair/typebox' -import { ok, fail } from './validate' - -describe("KeyOf", () => { - 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() - })) - ok(T, 'x') - ok(T, 'y') - ok(T, 'z') - fail(T, 'w') - }) - - it('Should validate when using pick', () => { - const T = Type.KeyOf(Type.Pick(Type.Object({ - x: Type.Number(), - y: Type.Number(), - z: Type.Number() - }), ['x', 'y'])) - ok(T, 'x') - ok(T, 'y') - fail(T, 'z') - }) - - it('Should validate when using omit', () => { - const T = Type.KeyOf(Type.Omit(Type.Object({ - x: Type.Number(), - y: Type.Number(), - z: Type.Number() - }), ['x', 'y'])) - - fail(T, 'x') - fail(T, 'y') - ok(T, 'z') - }) -}) diff --git a/spec/schema/literal.ts b/spec/schema/literal.ts deleted file mode 100644 index 72d190c1b..000000000 --- a/spec/schema/literal.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Type } from '@sinclair/typebox' -import { ok, fail } from './validate' -import * as assert from 'assert' - -describe("Literal", () => { - 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') - }) - - 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 deleted file mode 100644 index 456b72513..000000000 --- a/spec/schema/modifier.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Type, ReadonlyModifier, OptionalModifier } from '@sinclair/typebox' -import * as assert from 'assert' - -describe('Modifier', () => { - it('Omit modifier', () => { - 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) - }) -}) diff --git a/spec/schema/null.ts b/spec/schema/null.ts deleted file mode 100644 index 3ea7e8867..000000000 --- a/spec/schema/null.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Type } from '@sinclair/typebox' -import { ok, fail } from './validate' - -describe("Null", () => { - 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 deleted file mode 100644 index a37af99a1..000000000 --- a/spec/schema/number.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Type } from '@sinclair/typebox' -import { ok, fail } from './validate' - -describe("Number", () => { - 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 deleted file mode 100644 index e1e0285a7..000000000 --- a/spec/schema/object.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { Type } from '@sinclair/typebox' -import { ok, fail } from './validate' - -describe('Object', () => { - - it('Should not validate a number', () => { - const T = Type.Object({}) - fail(T, 42) - }) - - it('Should not validate a string', () => { - const T = Type.Object({}) - fail(T, 'hello') - }) - - it('Should not validate a boolean', () => { - const T = Type.Object({}) - fail(T, true) - }) - - it('Should not validate a null', () => { - const T = Type.Object({}) - fail(T, null) - }) - - it('Should not validate an array', () => { - const T = Type.Object({}) - fail(T, [1, 2]) - }) - - 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 } - }) - }) - - it('Should allow additionalProperties by default', () => { - const T = Type.Object({ - a: Type.Number(), - b: Type.String() - }) - ok(T, { - a: 1, - b: 'hello', - c: true - }) - }) - - 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 - }) - }) - - it('Should not allow properties for an empty object when additionalProperties is false', () => { - const T = Type.Object({}, { additionalProperties: false }) - ok(T, {}) - fail(T, { a: 10 }) - }) -}) diff --git a/spec/schema/omit.ts b/spec/schema/omit.ts deleted file mode 100644 index e4cb69d26..000000000 --- a/spec/schema/omit.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Type } from '@sinclair/typebox' -import { ok, fail } from './validate' -import { strictEqual } from 'assert' - -describe('Omit', () => { - it('Should omit properties on the source schema', () => { - const A = Type.Object({ - x: Type.Number(), - y: Type.Number(), - z: Type.Number() - }, { 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) - }) - - it('Should construct new object when targetting reference', () => { - const T = Type.Object({ a: Type.String(), b: Type.String() }, { $id: 'T' }) - const R = Type.Ref(T) - const P = Type.Omit(R, []) - strictEqual(P.properties.a.type, 'string') - strictEqual(P.properties.b.type, 'string') - }) -}) diff --git a/spec/schema/optional.ts b/spec/schema/optional.ts deleted file mode 100644 index 07168d943..000000000 --- a/spec/schema/optional.ts +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index 0e3af59f1..000000000 --- a/spec/schema/partial.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { OptionalModifier, ReadonlyOptionalModifier, Type } from '@sinclair/typebox' -import { ok, fail } from './validate' -import { strictEqual } from 'assert' - -describe('Partial', () => { - - it('Should convert a required object into a partial.', () => { - const A = Type.Object({ - x: Type.Number(), - y: Type.Number(), - z: Type.Number() - }, { 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('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() - }, { 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('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.Partial(A) - strictEqual(A.additionalProperties, false) - strictEqual(T.additionalProperties, false) - }) - - it('Should construct new object when targetting reference', () => { - const T = Type.Object({ a: Type.String(), b: Type.String() }, { $id: 'T' }) - const R = Type.Ref(T) - const P = Type.Partial(R) - strictEqual(P.properties.a.type, 'string') - strictEqual(P.properties.b.type, 'string') - }) -}) diff --git a/spec/schema/pick.ts b/spec/schema/pick.ts deleted file mode 100644 index 5318c3555..000000000 --- a/spec/schema/pick.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Type } from '@sinclair/typebox' -import { ok, fail } from './validate' -import { strictEqual } from 'assert' - -describe('Pick', () => { - it('Should pick properties from the source schema', () => { - const Vector3 = Type.Object({ - x: Type.Number(), - y: Type.Number(), - z: Type.Number() - }, { 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) - }) - it('Should construct new object when targetting reference', () => { - const T = Type.Object({ a: Type.String(), b: Type.String() }, { $id: 'T' }) - const R = Type.Ref(T) - const P = Type.Pick(R, ['a', 'b']) - strictEqual(P.properties.a.type, 'string') - strictEqual(P.properties.b.type, 'string') - }) -}) diff --git a/spec/schema/readonly-optional.ts b/spec/schema/readonly-optional.ts deleted file mode 100644 index 4edb7c9e2..000000000 --- a/spec/schema/readonly-optional.ts +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index 8cdc0f855..000000000 --- a/spec/schema/readonly.ts +++ /dev/null @@ -1,23 +0,0 @@ -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 deleted file mode 100644 index 51a4801e9..000000000 --- a/spec/schema/rec.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { Type } from '@sinclair/typebox' -import { ok, fail } from './validate' - -describe("Rec", () => { - - it('Should validate recursive Node type', () => { - const Node = Type.Rec(Self => Type.Object({ - nodeId: Type.String(), - nodes: Type.Array(Self) - }, { additionalProperties: false })) - ok(Node, { nodeId: '1', nodes: [] }) - ok(Node, { - nodeId: '1', - nodes: [ - { nodeId: '2', nodes: [] }, - { nodeId: '3', nodes: [] }, - { nodeId: '4', nodes: [] }, - { nodeId: '5', nodes: [] } - ] - }) - }) - it('Should validate recursive Node type with an $id', () => { - const Node = Type.Rec(Self => Type.Object({ - nodeId: Type.String(), - nodes: Type.Array(Self) - }, { additionalProperties: false }), - { $id: 'Node' }) - ok(Node, { nodeId: '1', nodes: [] }) - ok(Node, { - nodeId: '1', - nodes: [ - { nodeId: '2', nodes: [] }, - { nodeId: '3', nodes: [] }, - { nodeId: '4', nodes: [] }, - { nodeId: '5', nodes: [] } - ] - }) - }) - it('Should not validate recursive Node type with missing properties', () => { - const Node = Type.Rec(Self => Type.Object({ - nodeId: Type.String(), - nodes: Type.Array(Self) - }, { additionalProperties: false })) - fail(Node, { - nodes: [ - { nodeId: '2', nodes: [] }, - { nodeId: '3', nodes: [] }, - { nodeId: '4', nodes: [] }, - { nodeId: '5', nodes: [] } - ] - }) - }) - - it('Should not validate recursive Node type with additionalProperties', () => { - const Node = Type.Rec(Self => Type.Object({ - nodeId: Type.String(), - nodes: Type.Array(Self) - }, { additionalProperties: false })) - fail(Node, { - nodeId: '1', - nodes: [ - { nodeId: '2', nodes: [] }, - { nodeId: '3', nodes: [] }, - { nodeId: '4', nodes: [] }, - { nodeId: '5', nodes: [] } - ], - additional: 1 - }) - }) - - - // it('Should validate with JSON pointer references to sub schema', () => { - // const Element = Type.Rec('Element', Self => Type.Object({ - // elementId: Type.String(), - // elements: Type.Array(Self) - // }, { additionalProperties: false })) - - // const Node = Type.Rec('Node', Self => Type.Object({ - // nodeId: Type.String(), - // nodes: Type.Array(Self), - // element: Element - // }, { additionalProperties: false })) - - // ok(Node, { - // nodeId: '1', - // nodes: [ - // { nodeId: '2', nodes: [] }, - // { nodeId: '3', nodes: [] }, - // { nodeId: '4', nodes: [] }, - // { nodeId: '5', nodes: [] } - // ], - // element: { - // elementId: '1', - // elements: [{ - // elementId: '1', - // elements: [] - // }] - // } - // }) - // }) -}) diff --git a/spec/schema/record.ts b/spec/schema/record.ts deleted file mode 100644 index 187f2bcd6..000000000 --- a/spec/schema/record.ts +++ /dev/null @@ -1,100 +0,0 @@ -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 validate for keyof records', () => { - const T = Type.Object({ - a: Type.String(), - b: Type.Number(), - c: Type.String() - }) - const R = Type.Record(Type.KeyOf(T), Type.Number()) - ok(R, { a: 1, b: 2, c: 3 }) - }) - - 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/ref.ts b/spec/schema/ref.ts deleted file mode 100644 index 0eeee897b..000000000 --- a/spec/schema/ref.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { Type } from '@sinclair/typebox' -import { ok, fail } from './validate' - -describe('Ref', () => { - - it('Should should validate when referencing a type', () => { - const T = Type.Object({ - x: Type.Number(), - y: Type.Number(), - z: Type.Number() - }, { $id: 'T' }) - const R = Type.Ref(T) - ok(R, { - x: 1, - y: 2, - z: 3 - }, [T]) - }) - - it('Should not validate when passing invalid data', () => { - const T = Type.Object({ - x: Type.Number(), - y: Type.Number(), - z: Type.Number() - }, { $id: 'T' }) - const R = Type.Ref(T) - fail(R, { - x: 1, - y: 2 - }, [T]) - }) - - it('Should throw when not specifying an $id on target schema', () => { - try { - const T = Type.Object({ - x: Type.Number(), - y: Type.Number(), - z: Type.Number() - }, { }) - const R = Type.Ref(T) - } catch { - return - } - throw Error('Expected throw') - }) - - it('Should not validate when not adding additional schema', () => { - const T = Type.Object({ - x: Type.Number(), - y: Type.Number(), - z: Type.Number() - }, { $id: 'T' }) - const R = Type.Ref(T) - fail(R, { - x: 1, - y: 2, - z: 3 - }, []) - }) - - it('Should validate as a Box, and as a Ref', () => { - const Vertex = Type.Object({ - x: Type.Number(), - y: Type.Number(), - z: Type.Number() - }, { $id: 'Vertex' }) - - const Box = Type.Namespace({ - Vertex - }, { $id: 'Box' }) - - const R1 = Type.Ref(Vertex) - - const R2 = Type.Ref(Box, 'Vertex') - - ok(R1, { - x: 1, - y: 2, - z: 3 - }, [Box]) - ok(R2, { - x: 1, - y: 2, - z: 3 - }, [Box]) - }) -}) \ No newline at end of file diff --git a/spec/schema/regex.ts b/spec/schema/regex.ts deleted file mode 100644 index af2d52311..000000000 --- a/spec/schema/regex.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Type } from '@sinclair/typebox' -import { ok, fail } from './validate' - -describe('RegEx', () => { - - 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 deleted file mode 100644 index 2b9eb8cd8..000000000 --- a/spec/schema/required.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Type, ReadonlyModifier, ReadonlyOptionalModifier, OptionalModifier } from '@sinclair/typebox' -import { ok, fail } from './validate' -import { strictEqual } from 'assert' - -describe('Required', () => { - - 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()) - }, { 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('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 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('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()) - }, { additionalPropeties: false }) - const T = Type.Required(A) - strictEqual(A.additionalPropeties, false) - strictEqual(T.additionalPropeties, false) - }) - - it('Should construct new object when targetting reference', () => { - const T = Type.Object({ a: Type.String(), b: Type.String() }, { $id: 'T' }) - const R = Type.Ref(T) - const P = Type.Required(R) - strictEqual(P.properties.a.type, 'string') - strictEqual(P.properties.b.type, 'string') - }) -}) diff --git a/spec/schema/strict.ts b/spec/schema/strict.ts deleted file mode 100644 index 4329cd079..000000000 --- a/spec/schema/strict.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Type } from '@sinclair/typebox' -import { ok, fail } from './validate' - -describe('Required', () => { - - it('Should validate after applying strict', () => { - const A = Type.Object({ - x: Type.Optional(Type.Number()), - y: Type.Optional(Type.Number()), - z: Type.Optional(Type.Number()) - }, { additionalProperties: false }) - const T = Type.Strict(Type.Required(A)) - ok(T, { x: 1, y: 1, z: 1 }) - fail(T, { x: 1, y: 1 }) - fail(T, { x: 1 }) - fail(T, {}) - }) - - it('Should validate if applying $schema.', () => { - const A = Type.Object({ - x: Type.Optional(Type.Number()), - y: Type.Optional(Type.Number()), - z: Type.Optional(Type.Number()) - }, { additionalProperties: false }) - const $schema = 'https://json-schema.org/draft/2019-09/schema' - const T = Type.Strict(Type.Required(A), { $schema }) - ok(T, { x: 1, y: 1, z: 1 }) - fail(T, { x: 1, y: 1 }) - fail(T, { x: 1 }) - fail(T, {}) - }) - -}) diff --git a/spec/schema/string.ts b/spec/schema/string.ts deleted file mode 100644 index c51bb1c2c..000000000 --- a/spec/schema/string.ts +++ /dev/null @@ -1,49 +0,0 @@ -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('Should validate string format as email', () => { - const T = Type.String({ format: 'email' }) - ok(T, 'name@domain.com') - }) - - it('Should validate string format as uuid', () => { - const T = Type.String({ format: 'uuid' }) - ok(T, '4a7a17c9-2492-4a53-8e13-06ea2d3f3bbf') - }) - - 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 deleted file mode 100644 index 659de6565..000000000 --- a/spec/schema/tuple.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { Type } from '@sinclair/typebox' -import { isRegExp } from 'util' -import { ok, fail } from './validate' - -describe('Tuple', () => { - - 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]) - }) - - 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('Should validate empty tuple []', () => { - const T = Type.Tuple([]) - ok(T, []) - }) - - it('Should not validate empty tuple [] with one or more elements', () => { - const T = Type.Tuple([]) - fail(T, [1]) - }) - - 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 }, - ]) - }) - - 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 deleted file mode 100644 index c1e3c859a..000000000 --- a/spec/schema/union.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Type } from '@sinclair/typebox' -import { ok, fail } from './validate' - -describe('Union', () => { - - 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) - }) - - 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('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 - }) - - - 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('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 - }) - - 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 deleted file mode 100644 index aea8b4035..000000000 --- a/spec/schema/unknown.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Type } from '@sinclair/typebox' -import { ok, fail } from './validate' - -describe("Unknown", () => { - 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 deleted file mode 100644 index 50dd77f37..000000000 --- a/spec/schema/validate.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { TSchema } from '@sinclair/typebox' -import addFormats from 'ajv-formats' -import Ajv, {AnySchema} from 'ajv/dist/2019' - -export function validator(additional: AnySchema[]) { - 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') - .addSchema(additional) -} - - -export function ok(type: T, data: unknown, additional: AnySchema[] = []) { - const ajv = validator(additional) - function execute() { // required as ajv will throw if referenced schema is not found - try { return ajv.validate(type, data) as boolean } catch { return false } - } - if (execute() === 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, additional: AnySchema[] = []) { - const ajv = validator(additional) - function execute() { // required as ajv will throw if referenced schema is not found - try { return ajv.validate(type, data) as boolean } catch { return false } - } - if (execute() === 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('---------------------------') - console.log('errors') - console.log('---------------------------') - console.log(ajv.errorsText(ajv.errors)) - throw Error('expected ok') - } -} \ No newline at end of file diff --git a/spec/types/any.ts b/spec/types/any.ts deleted file mode 100644 index 00dcbbca0..000000000 --- a/spec/types/any.ts +++ /dev/null @@ -1,4 +0,0 @@ -import * as Spec from './spec' -import { Type } from './typebox' - -Spec.expectType(Spec.infer(Type.Any())) diff --git a/spec/types/array.ts b/spec/types/array.ts deleted file mode 100644 index 20e21336b..000000000 --- a/spec/types/array.ts +++ /dev/null @@ -1,22 +0,0 @@ -import * as Spec from './spec' -import { Type } from './typebox' - -const T0 = Type.Array(Type.String()) - -Spec.expectType(Spec.infer(T0)) - -const T1 = Type.Array(Type.Object({ - x: Type.Number(), - y: Type.Boolean(), - z: Type.String() -})) - -Spec.expectType<{x: number, y: boolean, z: string}[]>(Spec.infer(T1)) - -const T2 = Type.Array(Type.Array(Type.String())) - -Spec.expectType(Spec.infer(T2)) - -const T3 = Type.Array(Type.Tuple([Type.String(), Type.Number()])) - -Spec.expectType<[string, number][]>(Spec.infer(T3)) diff --git a/spec/types/boolean.ts b/spec/types/boolean.ts deleted file mode 100644 index 5f468bae3..000000000 --- a/spec/types/boolean.ts +++ /dev/null @@ -1,4 +0,0 @@ -import * as Spec from './spec' -import { Type } from './typebox' - -Spec.expectType(Spec.infer(Type.Boolean())) diff --git a/spec/types/enum.ts b/spec/types/enum.ts deleted file mode 100644 index cfeef50de..000000000 --- a/spec/types/enum.ts +++ /dev/null @@ -1,8 +0,0 @@ -import * as Spec from './spec' -import { Type } from './typebox' - -enum E { A, B = 'hello', C = 42 } - -const T = Type.Enum(E) - -Spec.expectType(Spec.infer(T)) \ No newline at end of file diff --git a/spec/types/intersect.ts b/spec/types/intersect.ts deleted file mode 100644 index f3fc6f9fd..000000000 --- a/spec/types/intersect.ts +++ /dev/null @@ -1,38 +0,0 @@ -import * as Spec from './spec' -import { Type, Static } from './typebox' - -{ - const A = Type.String() - const B = Type.Number() - const T = Type.Intersect([A, B]) - Spec.expectType(Spec.infer(T)) -} -{ - const A = Type.Object({ - A: Type.String(), - B: Type.String() - }) - const B = Type.Object({ - X: Type.Number(), - Y: Type.Number() - }) - const T = Type.Intersect([A, B]) - Spec.expectType<{ - A: string, - B: string - } & { - X: number, - Y: number - }>(Spec.infer(T)) -} -{ // https://github.com/sinclairzx81/typebox/issues/113 - const A = Type.Object({ A: Type.String() }) - const B = Type.Object({ B: Type.String() }) - const C = Type.Object({ C: Type.String() }) - const T = Type.Intersect([A, Type.Union([B, C])]) - type T = Static - const _0: T = { A: '', B: '' } - const _1: T = { A: '', C: '' } - const _3: T = { A: '', B: '', C: '' } - Spec.expectType<{ A: string } & ({ B: string, } | { C: string })>(Spec.infer(T)) -} diff --git a/spec/types/keyof.ts b/spec/types/keyof.ts deleted file mode 100644 index b9a830ff3..000000000 --- a/spec/types/keyof.ts +++ /dev/null @@ -1,49 +0,0 @@ -import * as Spec from './spec' -import { Type } from './typebox' - -{ - const K = Type.KeyOf( - Type.Object({ - A: Type.Null(), - B: Type.Null(), - C: Type.Null(), - }) - ) - - Spec.expectType<'A' | 'B' | 'C'>(Spec.infer(K)) -} - -{ - const Q = Type.Pick( - Type.Object({ - A: Type.Null(), - B: Type.Null(), - C: Type.Null(), - }), ['A', 'B'] - ) - - const K = Type.KeyOf( - Type.Pick( - Type.Object({ - A: Type.Null(), - B: Type.Null(), - C: Type.Null(), - }), ['A', 'B'] - ) - ) - - Spec.expectType<'A' | 'B'>(Spec.infer(K)) -} - -{ - const K = Type.KeyOf( - Type.Omit( - Type.Object({ - A: Type.Null(), - B: Type.Null(), - C: Type.Null(), - }), ['A', 'B'] - ) - ) - Spec.expectType<'C'>(Spec.infer(K)) -} \ No newline at end of file diff --git a/spec/types/literal.ts b/spec/types/literal.ts deleted file mode 100644 index 982584675..000000000 --- a/spec/types/literal.ts +++ /dev/null @@ -1,8 +0,0 @@ -import * as Spec from './spec' -import { Type } from './typebox' - -Spec.expectType<'hello'>(Spec.infer(Type.Literal('hello'))) - -Spec.expectType(Spec.infer(Type.Literal(true))) - -Spec.expectType<42>(Spec.infer(Type.Literal(42))) \ No newline at end of file diff --git a/spec/types/modifier.ts b/spec/types/modifier.ts deleted file mode 100644 index 4b1ae11fc..000000000 --- a/spec/types/modifier.ts +++ /dev/null @@ -1,18 +0,0 @@ -import * as Spec from './spec' -import { Type } from './typebox' - -// Asserts combinatory modifiers -{ - const T = Type.Object({ - A: Type.ReadonlyOptional(Type.String()), - B: Type.Readonly(Type.String()), - C: Type.Optional(Type.String()), - D: Type.String() - }) - Spec.expectType<{ - readonly A?: string, - readonly B: string, - C?: string, - D: string - }>(Spec.infer(T)) -} \ No newline at end of file diff --git a/spec/types/namespace.ts b/spec/types/namespace.ts deleted file mode 100644 index 5e580ed82..000000000 --- a/spec/types/namespace.ts +++ /dev/null @@ -1,16 +0,0 @@ -import * as Spec from './spec' -import { Type } from './typebox' - -{ - const T = Type.Namespace({ - Vector2: Type.Object({ - X: Type.Number(), - Y: Type.Number(), - }) - }, { $id: 'Math' }) - - Spec.expectType<{ - X: number, - Y: number - }>(Spec.infer(T['definitions']['Vector2'])) -} \ No newline at end of file diff --git a/spec/types/null.ts b/spec/types/null.ts deleted file mode 100644 index e3a49fa0e..000000000 --- a/spec/types/null.ts +++ /dev/null @@ -1,4 +0,0 @@ -import * as Spec from './spec' -import { Type } from './typebox' - -Spec.expectType(Spec.infer(Type.Null())) diff --git a/spec/types/number.ts b/spec/types/number.ts deleted file mode 100644 index 9c3a761b5..000000000 --- a/spec/types/number.ts +++ /dev/null @@ -1,5 +0,0 @@ -import * as Spec from './spec' -import { Type } from './typebox' - -Spec.expectType(Spec.infer(Type.Number())) - diff --git a/spec/types/object.ts b/spec/types/object.ts deleted file mode 100644 index 3b112a608..000000000 --- a/spec/types/object.ts +++ /dev/null @@ -1,54 +0,0 @@ -import * as Spec from './spec' -import { Type } from './typebox' - -{ - const T = Type.Object({ - A: Type.String(), - B: Type.String(), - C: Type.String() - }) - - Spec.expectType<{ - A: string, - B: string, - C: string - }>(Spec.infer(T)) -} -{ - const T = Type.Object({ - A: Type.Object({ - A: Type.String(), - B: Type.String(), - C: Type.String() - }), - B: Type.Object({ - A: Type.String(), - B: Type.String(), - C: Type.String() - }), - C: Type.Object({ - A: Type.String(), - B: Type.String(), - C: Type.String() - }) - }) - Spec.expectType<{ - A: { - A: string, - B: string, - C: string - }, - B: { - A: string, - B: string, - C: string - }, - C: { - A: string, - B: string, - C: string - } - }>(Spec.infer(T)) -} - - diff --git a/spec/types/omit.ts b/spec/types/omit.ts deleted file mode 100644 index 5614036b2..000000000 --- a/spec/types/omit.ts +++ /dev/null @@ -1,17 +0,0 @@ -import * as Spec from './spec' -import { Type } from './typebox' - -{ - const T = Type.Omit( - Type.Object({ - A: Type.String(), - B: Type.String(), - C: Type.String() - }), - ['C']) - - Spec.expectType<{ - A: string, - B: string - }>(Spec.infer(T)) -} \ No newline at end of file diff --git a/spec/types/optional.ts b/spec/types/optional.ts deleted file mode 100644 index 1de0fd3c4..000000000 --- a/spec/types/optional.ts +++ /dev/null @@ -1,11 +0,0 @@ -import * as Spec from './spec' -import { Type } from './typebox' - -{ - const T = Type.Object({ - A: Type.Optional(Type.String()) - }) - Spec.expectType<{ - A?: string - }>(Spec.infer(T)) -} \ No newline at end of file diff --git a/spec/types/partial.ts b/spec/types/partial.ts deleted file mode 100644 index 63ee73fbc..000000000 --- a/spec/types/partial.ts +++ /dev/null @@ -1,18 +0,0 @@ -import * as Spec from './spec' -import { Type } from './typebox' - -{ - const T = Type.Partial( - Type.Object({ - A: Type.String(), - B: Type.String(), - C: Type.String() - }), - ) - - Spec.expectType<{ - A?: string, - B?: string, - C?: string - }>(Spec.infer(T)) -} \ No newline at end of file diff --git a/spec/types/pick.ts b/spec/types/pick.ts deleted file mode 100644 index 73c4ac527..000000000 --- a/spec/types/pick.ts +++ /dev/null @@ -1,17 +0,0 @@ -import * as Spec from './spec' -import { Type } from './typebox' - -{ - const T = Type.Pick( - Type.Object({ - A: Type.String(), - B: Type.String(), - C: Type.String() - }), - ['A', 'B']) - - Spec.expectType<{ - A: string, - B: string - }>(Spec.infer(T)) -} \ No newline at end of file diff --git a/spec/types/readonly-optional.ts b/spec/types/readonly-optional.ts deleted file mode 100644 index 59b460adf..000000000 --- a/spec/types/readonly-optional.ts +++ /dev/null @@ -1,11 +0,0 @@ -import * as Spec from './spec' -import { Type } from './typebox' - -{ - const T = Type.Object({ - A: Type.ReadonlyOptional(Type.String()) - }) - Spec.expectType<{ - readonly A?: string - }>(Spec.infer(T)) -} \ No newline at end of file diff --git a/spec/types/readonly.ts b/spec/types/readonly.ts deleted file mode 100644 index 9b1ff7f24..000000000 --- a/spec/types/readonly.ts +++ /dev/null @@ -1,12 +0,0 @@ -import * as Spec from './spec' -import { Type } from './typebox' - -{ - const T = Type.Object({ - A: Type.Readonly(Type.String()) - }) - - Spec.expectType<{ - readonly A: string - }>(Spec.infer(T)) -} \ No newline at end of file diff --git a/spec/types/rec.ts b/spec/types/rec.ts deleted file mode 100644 index 131c68049..000000000 --- a/spec/types/rec.ts +++ /dev/null @@ -1,14 +0,0 @@ -import * as Spec from './spec' -import { Type } from './typebox' - -{ - const T = Type.Rec(Self => Type.Object({ - id: Type.String(), - nodes: Type.Array(Self) - })) - - Spec.expectType<{ - id: string, - nodes: { id: string, nodes: [] }[] - }>(Spec.infer(T)) -} \ No newline at end of file diff --git a/spec/types/record.ts b/spec/types/record.ts deleted file mode 100644 index 0d02cd207..000000000 --- a/spec/types/record.ts +++ /dev/null @@ -1,85 +0,0 @@ -import * as Spec from './spec' -import { Type } from './typebox' -{ - // type K = string - const K = Type.String() - - Spec.expectType< - Record - >(Spec.infer( - Type.Record(K, Type.Number())) - ) -} -{ - // type K = string - const K = Type.RegEx(/foo|bar/) - - Spec.expectType< - Record - >(Spec.infer( - Type.Record(K, Type.Number())) - ) -} -{ - // type K = number - const K = Type.Number() - - Spec.expectType< - Record - >(Spec.infer( - Type.Record(K, Type.Number())) - ) - - Spec.expectType< - Record - >(Spec.infer( - Type.Record(K, Type.Number())) - ) -} -{ - // type K = 'A' | 'B' | 'C' - const K = Type.Union([ - Type.Literal('A'), - Type.Literal('B'), - Type.Literal('C') - ]) - - Spec.expectType< - Record<'A' | 'B' | 'C', number> - >(Spec.infer( - Type.Record(K, Type.Number())) - ) -} -{ - // type K = keyof { A: number, B: number, C: number } - const K = Type.KeyOf( - Type.Object({ - A: Type.Number(), - B: Type.Number(), - C: Type.Number() - }) - ) - Spec.expectType< - Record<'A' | 'B' | 'C', number> - >(Spec.infer( - Type.Record(K, Type.Number())) - ) -} -{ - // type K = keyof Omit<{ A: number, B: number, C: number }, 'C'> - const K = Type.KeyOf( - Type.Omit( - Type.Object({ - A: Type.Number(), - B: Type.Number(), - C: Type.Number() - }), - ['C'] - ) - ) - Spec.expectType< - Record<'A' | 'B', number> - >(Spec.infer( - Type.Record(K, Type.Number())) - ) -} \ No newline at end of file diff --git a/spec/types/ref.ts b/spec/types/ref.ts deleted file mode 100644 index 2089da146..000000000 --- a/spec/types/ref.ts +++ /dev/null @@ -1,26 +0,0 @@ -import * as Spec from './spec' -import { Type } from './typebox' - -{ - const T = Type.String({ $id: 'T' }) - - const R = Type.Ref(T) - - Spec.expectType(Spec.infer(R)) - -} -{ - const T = Type.Namespace({ - Vector2: Type.Object({ - X: Type.Number(), - Y: Type.Number(), - }) - }, { $id: 'Math' }) - - const R = Type.Ref(T, 'Vector2') - - Spec.expectType<{ - X: number, - Y: number - }>(Spec.infer(R)) -} \ No newline at end of file diff --git a/spec/types/regex.ts b/spec/types/regex.ts deleted file mode 100644 index 4bfabc9af..000000000 --- a/spec/types/regex.ts +++ /dev/null @@ -1,4 +0,0 @@ -import * as Spec from './spec' -import { Type } from './typebox' - -Spec.expectType(Spec.infer(Type.RegEx(/foo/))) diff --git a/spec/types/required.ts b/spec/types/required.ts deleted file mode 100644 index f4f0dc5fe..000000000 --- a/spec/types/required.ts +++ /dev/null @@ -1,18 +0,0 @@ -import * as Spec from './spec' -import { Type, Static } from './typebox' - -{ - const T = Type.Required( - Type.Object({ - A: Type.Optional(Type.String()), - B: Type.Optional(Type.String()), - C: Type.Optional(Type.String()) - }), - ) - - Spec.expectType<{ - A: string, - B: string, - C: string - }>(Spec.infer(T)) -} \ No newline at end of file diff --git a/spec/types/spec.ts b/spec/types/spec.ts deleted file mode 100644 index 2f7f31bea..000000000 --- a/spec/types/spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { TSchema, Static } from './typebox' - -export const infer = (_: T): Static => null as any as Static - -export * from 'tsd' - - - diff --git a/spec/types/strict.ts b/spec/types/strict.ts deleted file mode 100644 index 24dba3044..000000000 --- a/spec/types/strict.ts +++ /dev/null @@ -1,16 +0,0 @@ -import * as Spec from './spec' -import { Type } from './typebox' - -{ - const T = Type.Strict(Type.Object({ - A: Type.String(), - B: Type.String(), - C: Type.String() - })) - - Spec.expectType<{ - A: string, - B: string, - C: string - }>(Spec.infer(T)) -} \ No newline at end of file diff --git a/spec/types/string.ts b/spec/types/string.ts deleted file mode 100644 index f4e8578bb..000000000 --- a/spec/types/string.ts +++ /dev/null @@ -1,4 +0,0 @@ -import * as Spec from './spec' -import { Type } from './typebox' - -Spec.expectType(Spec.infer(Type.String())) diff --git a/spec/types/tuple.ts b/spec/types/tuple.ts deleted file mode 100644 index db323731d..000000000 --- a/spec/types/tuple.ts +++ /dev/null @@ -1,12 +0,0 @@ -import * as Spec from './spec' -import { Type } from './typebox' - -{ - const T = Type.Tuple([ - Type.Number(), - Type.String(), - Type.Boolean() - ]) - - Spec.expectType<[number, string, boolean]>(Spec.infer(T)) -} diff --git a/spec/types/typebox.d.ts b/spec/types/typebox.d.ts deleted file mode 100644 index df723db3a..000000000 --- a/spec/types/typebox.d.ts +++ /dev/null @@ -1,348 +0,0 @@ -export declare const ReadonlyOptionalModifier: unique symbol; -export declare const OptionalModifier: unique symbol; -export declare const ReadonlyModifier: unique symbol; -export declare type TModifier = TReadonlyOptional | TOptional | TReadonly; -export declare type TReadonlyOptional = T & { - modifier: typeof ReadonlyOptionalModifier; -}; -export declare type TOptional = T & { - modifier: typeof OptionalModifier; -}; -export declare type TReadonly = T & { - modifier: typeof ReadonlyModifier; -}; -export declare const BoxKind: unique symbol; -export declare const KeyOfKind: unique symbol; -export declare const IntersectKind: unique symbol; -export declare const UnionKind: unique symbol; -export declare const TupleKind: unique symbol; -export declare const ObjectKind: unique symbol; -export declare const RecordKind: unique symbol; -export declare const ArrayKind: unique symbol; -export declare const EnumKind: unique symbol; -export declare const LiteralKind: unique symbol; -export declare const StringKind: unique symbol; -export declare const NumberKind: unique symbol; -export declare const IntegerKind: unique symbol; -export declare const BooleanKind: unique symbol; -export declare const NullKind: unique symbol; -export declare const UnknownKind: unique symbol; -export declare const AnyKind: unique symbol; -export declare const RefKind: unique symbol; -export interface CustomOptions { - $id?: string; - title?: string; - description?: string; - default?: any; - examples?: any; - [prop: string]: any; -} -export declare type StringFormatOption = 'date-time' | 'time' | 'date' | 'email' | 'idn-email' | 'hostname' | 'idn-hostname' | 'ipv4' | 'ipv6' | 'uri' | 'uri-reference' | 'iri' | 'uuid' | 'iri-reference' | 'uri-template' | 'json-pointer' | 'relative-json-pointer' | 'regex'; -export declare type StringOptions = { - minLength?: number; - maxLength?: number; - pattern?: string; - format?: TFormat; - contentEncoding?: '7bit' | '8bit' | 'binary' | 'quoted-printable' | 'base64'; - contentMediaType?: string; -} & CustomOptions; -export declare type ArrayOptions = { - uniqueItems?: boolean; - minItems?: number; - maxItems?: number; -} & CustomOptions; -export declare type NumberOptions = { - exclusiveMaximum?: number; - exclusiveMinimum?: number; - maximum?: number; - minimum?: number; - multipleOf?: number; -} & CustomOptions; -export declare type IntersectOptions = { - unevaluatedProperties?: boolean; -} & CustomOptions; -export declare type ObjectOptions = { - additionalProperties?: boolean; -} & CustomOptions; -export declare type TDefinitions = { - [key: string]: TSchema; -}; -export declare type TNamespace = { - kind: typeof BoxKind; - $defs: T; -} & CustomOptions; -export interface TSchema { - $static: unknown; -} -export declare type TEnumType = Record; -export declare type TKey = string | number | symbol; -export declare type TValue = string | number | boolean; -export declare type TRecordKey = TString | TNumber | TKeyOf | TUnion; -export declare type TEnumKey = { - type: 'number' | 'string'; - const: T; -}; -export interface TProperties { - [key: string]: TSchema; -} -export interface TRecord extends TSchema, ObjectOptions { - $static: StaticRecord; - kind: typeof RecordKind; - type: 'object'; - patternProperties: { - [pattern: string]: T; - }; -} -export interface TTuple extends TSchema, CustomOptions { - $static: StaticTuple; - kind: typeof TupleKind; - type: 'array'; - items?: T; - additionalItems?: false; - minItems: number; - maxItems: number; -} -export interface TObject extends TSchema, ObjectOptions { - $static: StaticObject; - kind: typeof ObjectKind; - type: 'object'; - properties: T; - required?: string[]; -} -export interface TUnion extends TSchema, CustomOptions { - $static: StaticUnion; - kind: typeof UnionKind; - anyOf: T; -} -export interface TIntersect extends TSchema, IntersectOptions { - $static: StaticIntersect; - kind: typeof IntersectKind; - type: 'object'; - allOf: T; -} -export interface TKeyOf extends TSchema, CustomOptions { - $static: StaticKeyOf; - kind: typeof KeyOfKind; - type: 'string'; - enum: T; -} -export interface TArray extends TSchema, ArrayOptions { - $static: StaticArray; - kind: typeof ArrayKind; - type: 'array'; - items: T; -} -export interface TLiteral extends TSchema, CustomOptions { - $static: StaticLiteral; - kind: typeof LiteralKind; - const: T; -} -export interface TEnum extends TSchema, CustomOptions { - $static: StaticEnum; - kind: typeof EnumKind; - anyOf: T; -} -export interface TString extends TSchema, StringOptions { - $static: string; - kind: typeof StringKind; - type: 'string'; -} -export interface TNumber extends TSchema, NumberOptions { - $static: number; - kind: typeof NumberKind; - type: 'number'; -} -export interface TInteger extends TSchema, NumberOptions { - $static: number; - kind: typeof IntegerKind; - type: 'integer'; -} -export interface TBoolean extends TSchema, CustomOptions { - $static: boolean; - kind: typeof BooleanKind; - type: 'boolean'; -} -export interface TNull extends TSchema, CustomOptions { - $static: null; - kind: typeof NullKind; - type: 'null'; -} -export interface TUnknown extends TSchema, CustomOptions { - $static: unknown; - kind: typeof UnknownKind; -} -export interface TAny extends TSchema, CustomOptions { - $static: any; - kind: typeof AnyKind; -} -export interface TRef extends TSchema, CustomOptions { - $static: Static; - kind: typeof RefKind; - $ref: string; -} -export declare const ConstructorKind: unique symbol; -export declare const FunctionKind: unique symbol; -export declare const PromiseKind: unique symbol; -export declare const UndefinedKind: unique symbol; -export declare const VoidKind: unique symbol; -export interface TConstructor extends TSchema, CustomOptions { - $static: StaticConstructor; - kind: typeof ConstructorKind; - type: 'constructor'; - arguments: TSchema[]; - returns: TSchema; -} -export interface TFunction extends TSchema, CustomOptions { - $static: StaticFunction; - kind: typeof FunctionKind; - type: 'function'; - arguments: TSchema[]; - returns: TSchema; -} -export interface TPromise extends TSchema, CustomOptions { - $static: StaticPromise; - kind: typeof PromiseKind; - type: 'promise'; - item: TSchema; -} -export interface TUndefined extends TSchema, CustomOptions { - $static: undefined; - kind: typeof UndefinedKind; - type: 'undefined'; -} -export interface TVoid extends TSchema, CustomOptions { - $static: void; - kind: typeof VoidKind; - type: 'void'; -} -export declare type Pickable = TObject | TRef>; -export declare type PickablePropertyKeys = T extends TObject ? keyof U : T extends TRef> ? keyof U : never; -export declare type PickableProperties = T extends TObject ? U : T extends TRef> ? U : never; -export declare type UnionToIntersect = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never; -export declare type StaticReadonlyOptionalPropertyKeys = { - [K in keyof T]: T[K] extends TReadonlyOptional ? K : never; -}[keyof T]; -export declare type StaticReadonlyPropertyKeys = { - [K in keyof T]: T[K] extends TReadonly ? K : never; -}[keyof T]; -export declare type StaticOptionalPropertyKeys = { - [K in keyof T]: T[K] extends TOptional ? K : never; -}[keyof T]; -export declare type StaticRequiredPropertyKeys = keyof Omit | StaticReadonlyPropertyKeys | StaticOptionalPropertyKeys>; -export declare type StaticIntersectEvaluate = { - [K in keyof T]: T[K] extends TSchema ? Static : never; -}; -export declare type StaticIntersectReduce = T extends [infer A, ...infer B] ? StaticIntersectReduce : I; -export declare type StaticRequired = { - [K in keyof T]: T[K] extends TReadonlyOptional ? TReadonly : T[K] extends TReadonly ? TReadonly : T[K] extends TOptional ? U : T[K]; -}; -export declare type StaticPartial = { - [K in keyof T]: T[K] extends TReadonlyOptional ? TReadonlyOptional : T[K] extends TReadonly ? TReadonlyOptional : T[K] extends TOptional ? TOptional : TOptional; -}; -export declare type StaticProperties = { - readonly [K in StaticReadonlyOptionalPropertyKeys]?: Static; -} & { - readonly [K in StaticReadonlyPropertyKeys]: Static; -} & { - [K in StaticOptionalPropertyKeys]?: Static; -} & { - [K in StaticRequiredPropertyKeys]: Static; -}; -export declare type StaticRecord = K extends TString ? Record> : K extends TNumber ? Record> : K extends TKeyOf ? Record> : K extends TUnion ? Record> : never; -export declare type StaticEnum = T extends TEnumKey[] ? U : never; -export declare type StaticKeyOf = T extends Array ? K : never; -export declare type StaticIntersect = StaticIntersectReduce>; -export declare type StaticUnion = { - [K in keyof T]: T[K] extends TSchema ? Static : never; -}[number]; -export declare type StaticTuple = { - [K in keyof T]: T[K] extends TSchema ? Static : never; -}; -export declare type StaticObject = StaticProperties extends infer I ? { - [K in keyof I]: I[K]; -} : never; -export declare type StaticArray = Array>; -export declare type StaticLiteral = T; -export declare type StaticParameters = { - [K in keyof T]: T[K] extends TSchema ? Static : never; -}; -export declare type StaticConstructor = new (...args: [...StaticParameters]) => Static; -export declare type StaticFunction = (...args: [...StaticParameters]) => Static; -export declare type StaticPromise = Promise>; -export declare type Static = T['$static']; -export declare class TypeBuilder { - private readonly schemas; - /** `Standard` Modifies an object property to be both readonly and optional */ - ReadonlyOptional(item: T): TReadonlyOptional; - /** `Standard` Modifies an object property to be readonly */ - Readonly(item: T): TReadonly; - /** `Standard` Modifies an object property to be optional */ - Optional(item: T): TOptional; - /** `Standard` Creates a type type */ - Tuple(items: [...T], options?: CustomOptions): TTuple; - /** `Standard` Creates an object type with the given properties */ - Object(properties: T, options?: ObjectOptions): TObject; - /** `Standard` Creates an intersect type. */ - Intersect(items: [...T], options?: IntersectOptions): TIntersect; - /** `Standard` Creates a union type */ - Union(items: [...T], options?: CustomOptions): TUnion; - /** `Standard` Creates an array type */ - Array(items: T, options?: ArrayOptions): TArray; - /** `Standard` Creates an enum type from a TypeScript enum */ - Enum(item: T, options?: CustomOptions): TEnum[]>; - /** `Standard` Creates a literal type. Supports string, number and boolean values only */ - Literal(value: T, options?: CustomOptions): TLiteral; - /** `Standard` Creates a string type */ - String(options?: StringOptions): TString; - /** `Standard` Creates a string type from a regular expression */ - RegEx(regex: RegExp, options?: CustomOptions): TString; - /** `Standard` Creates a number type */ - Number(options?: NumberOptions): TNumber; - /** `Standard` Creates an integer type */ - Integer(options?: NumberOptions): TInteger; - /** `Standard` Creates a boolean type */ - Boolean(options?: CustomOptions): TBoolean; - /** `Standard` Creates a null type */ - Null(options?: CustomOptions): TNull; - /** `Standard` Creates an unknown type */ - Unknown(options?: CustomOptions): TUnknown; - /** `Standard` Creates an any type */ - Any(options?: CustomOptions): TAny; - /** `Standard` Creates a keyof type from the given object */ - KeyOf>(object: T, options?: CustomOptions): TKeyOf<(keyof T['properties'])[]>; - /** `Standard` Creates a record type */ - Record(key: K, value: T, options?: ObjectOptions): TRecord; - /** `Standard` Makes all properties in the given object type required */ - Required | TRef>>(object: T, options?: ObjectOptions): TObject>; - /** `Standard` Makes all properties in the given object type optional */ - Partial | TRef>>(object: T, options?: ObjectOptions): TObject>; - /** `Standard` Picks property keys from the given object type */ - Pick | TRef>, K extends PickablePropertyKeys[]>(object: T, keys: [...K], options?: ObjectOptions): TObject, K[number]>>; - /** `Standard` Omits property keys from the given object type */ - Omit | TRef>, K extends PickablePropertyKeys[]>(object: T, keys: [...K], options?: ObjectOptions): TObject, K[number]>>; - /** `Standard` Omits the `kind` and `modifier` properties from the underlying schema */ - Strict(schema: T, options?: CustomOptions): T; - /** `Extended` Creates a constructor type */ - Constructor(args: [...T], returns: U, options?: CustomOptions): TConstructor; - /** `Extended` Creates a function type */ - Function(args: [...T], returns: U, options?: CustomOptions): TFunction; - /** `Extended` Creates a promise type */ - Promise(item: T, options?: CustomOptions): TPromise; - /** `Extended` Creates a undefined type */ - Undefined(options?: CustomOptions): TUndefined; - /** `Extended` Creates a void type */ - Void(options?: CustomOptions): TVoid; - /** `Standard` Creates a namespace for a set of related types */ - Namespace($defs: T, options?: CustomOptions): TNamespace; - /** `Standard` References a type within a namespace. The referenced namespace must specify an `$id` */ - Ref, K extends keyof T['$defs']>(box: T, key: K): TRef; - /** `Standard` References type. The referenced type must specify an `$id` */ - Ref(schema: T): TRef; - /** `Experimental` Creates a recursive type */ - Rec(callback: (self: TAny) => T, options?: CustomOptions): T; - /** Stores this schema if it contains an $id. This function is used for later referencing. */ - private Store; - /** Resolves a schema by $id. May resolve recursively if the target is a TRef. */ - private Resolve; -} -export declare const Type: TypeBuilder; diff --git a/spec/types/union.ts b/spec/types/union.ts deleted file mode 100644 index 892a25e74..000000000 --- a/spec/types/union.ts +++ /dev/null @@ -1,28 +0,0 @@ -import * as Spec from './spec' -import { Type } from './typebox' - -{ - const A = Type.String() - const B = Type.Number() - const T = Type.Union([A, B]) - Spec.expectType(Spec.infer(T)) -} -{ - const A = Type.Object({ - A: Type.String(), - B: Type.String() - }) - const B = Type.Object({ - X: Type.Number(), - Y: Type.Number() - }) - const T = Type.Union([A, B]) - Spec.expectType<{ - A: string, - B: string - } | { - X: number, - Y: number - }>(Spec.infer(T)) -} - diff --git a/spec/types/unknown.ts b/spec/types/unknown.ts deleted file mode 100644 index 45a99a05e..000000000 --- a/spec/types/unknown.ts +++ /dev/null @@ -1,4 +0,0 @@ -import * as Spec from './spec' -import { Type } from './typebox' - -Spec.expectType(Spec.infer(Type.Unknown())) \ No newline at end of file diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts new file mode 100644 index 000000000..05e2e905d --- /dev/null +++ b/src/compiler/compiler.ts @@ -0,0 +1,665 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/compiler + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { TransformEncode, TransformDecode, HasTransform, TransformDecodeCheckError, TransformEncodeCheckError } from '../value/transform/index' +import { Errors, ValueErrorIterator } from '../errors/index' +import { TypeSystemPolicy } from '../system/index' +import { TypeBoxError } from '../type/error/index' +import { Deref } from '../value/deref/index' +import { Hash } from '../value/hash/index' +import { Kind } from '../type/symbols/index' + +import { TypeRegistry, FormatRegistry } from '../type/registry/index' +import { KeyOfPattern } from '../type/keyof/index' +import { ExtendsUndefinedCheck } from '../type/extends/extends-undefined' + +import type { TSchema } from '../type/schema/index' +import type { TAsyncIterator } from '../type/async-iterator/index' +import type { TAny } from '../type/any/index' +import type { TArgument } from '../type/argument/index' +import type { TArray } from '../type/array/index' +import type { TBigInt } from '../type/bigint/index' +import type { TBoolean } from '../type/boolean/index' +import type { TDate } from '../type/date/index' +import type { TConstructor } from '../type/constructor/index' +import type { TFunction } from '../type/function/index' +import type { TImport } from '../type/module/index' +import type { TInteger } from '../type/integer/index' +import type { TIntersect } from '../type/intersect/index' +import type { TIterator } from '../type/iterator/index' +import type { TLiteral } from '../type/literal/index' +import { Never, type TNever } from '../type/never/index' +import type { TNot } from '../type/not/index' +import type { TNull } from '../type/null/index' +import type { TNumber } from '../type/number/index' +import type { TObject } from '../type/object/index' +import type { TPromise } from '../type/promise/index' +import type { TRecord } from '../type/record/index' +import { Ref, type TRef } from '../type/ref/index' +import type { TRegExp } from '../type/regexp/index' +import type { TTemplateLiteral } from '../type/template-literal/index' +import type { TThis } from '../type/recursive/index' +import type { TTuple } from '../type/tuple/index' +import type { TUnion } from '../type/union/index' +import type { TUnknown } from '../type/unknown/index' +import type { Static, StaticDecode, StaticEncode } from '../type/static/index' +import type { TString } from '../type/string/index' +import type { TSymbol } from '../type/symbol/index' +import type { TUndefined } from '../type/undefined/index' +import type { TUint8Array } from '../type/uint8array/index' +import type { TVoid } from '../type/void/index' + +// ------------------------------------------------------------------ +// ValueGuard +// ------------------------------------------------------------------ +import { IsArray, IsString, IsNumber, IsBigInt } from '../value/guard/index' +// ------------------------------------------------------------------ +// TypeGuard +// ------------------------------------------------------------------ +import { IsSchema } from '../type/guard/type' +// ------------------------------------------------------------------ +// CheckFunction +// ------------------------------------------------------------------ +export type CheckFunction = (value: unknown) => boolean +// ------------------------------------------------------------------ +// TypeCheck +// ------------------------------------------------------------------ +export class TypeCheck { + private readonly hasTransform: boolean + constructor(private readonly schema: T, private readonly references: TSchema[], private readonly checkFunc: CheckFunction, private readonly code: string) { + this.hasTransform = HasTransform(schema, references) + } + /** Returns the generated assertion code used to validate this type. */ + public Code(): string { + return this.code + } + /** Returns the schema type used to validate */ + public Schema(): T { + return this.schema + } + /** Returns reference types used to validate */ + public References(): TSchema[] { + return this.references + } + /** Returns an iterator for each error in this value. */ + public Errors(value: unknown): ValueErrorIterator { + return Errors(this.schema, this.references, value) + } + /** Returns true if the value matches the compiled type. */ + public Check(value: unknown): value is Static { + return this.checkFunc(value) + } + /** Decodes a value or throws if error */ + public Decode, Result extends Static = Static>(value: unknown): Result { + if (!this.checkFunc(value)) throw new TransformDecodeCheckError(this.schema, value, this.Errors(value).First()!) + return (this.hasTransform ? TransformDecode(this.schema, this.references, value) : value) as never + } + /** Encodes a value or throws if error */ + public Encode, Result extends Static = Static>(value: unknown): Result { + const encoded = this.hasTransform ? TransformEncode(this.schema, this.references, value) : value + if (!this.checkFunc(encoded)) throw new TransformEncodeCheckError(this.schema, value, this.Errors(value).First()!) + return encoded as never + } +} +// ------------------------------------------------------------------ +// Character +// ------------------------------------------------------------------ +namespace Character { + export function DollarSign(code: number) { + return code === 36 + } + export function IsUnderscore(code: number) { + return code === 95 + } + export function IsAlpha(code: number) { + return (code >= 65 && code <= 90) || (code >= 97 && code <= 122) + } + export function IsNumeric(code: number) { + return code >= 48 && code <= 57 + } +} +// ------------------------------------------------------------------ +// MemberExpression +// ------------------------------------------------------------------ +namespace MemberExpression { + function IsFirstCharacterNumeric(value: string) { + if (value.length === 0) return false + return Character.IsNumeric(value.charCodeAt(0)) + } + function IsAccessor(value: string) { + if (IsFirstCharacterNumeric(value)) return false + for (let i = 0; i < value.length; i++) { + const code = value.charCodeAt(i) + const check = Character.IsAlpha(code) || Character.IsNumeric(code) || Character.DollarSign(code) || Character.IsUnderscore(code) + if (!check) return false + } + return true + } + function EscapeHyphen(key: string) { + return key.replace(/'/g, "\\'") + } + export function Encode(object: string, key: string) { + return IsAccessor(key) ? `${object}.${key}` : `${object}['${EscapeHyphen(key)}']` + } +} +// ------------------------------------------------------------------ +// Identifier +// ------------------------------------------------------------------ +namespace Identifier { + export function Encode($id: string) { + const buffer: string[] = [] + for (let i = 0; i < $id.length; i++) { + const code = $id.charCodeAt(i) + if (Character.IsNumeric(code) || Character.IsAlpha(code)) { + buffer.push($id.charAt(i)) + } else { + buffer.push(`_${code}_`) + } + } + return buffer.join('').replace(/__/g, '_') + } +} +// ------------------------------------------------------------------ +// LiteralString +// ------------------------------------------------------------------ +namespace LiteralString { + export function Escape(content: string) { + return content.replace(/'/g, "\\'") + } +} +// ------------------------------------------------------------------ +// Errors +// ------------------------------------------------------------------ +export class TypeCompilerUnknownTypeError extends TypeBoxError { + constructor(public readonly schema: TSchema) { + super('Unknown type') + } +} +export class TypeCompilerTypeGuardError extends TypeBoxError { + constructor(public readonly schema: TSchema) { + super('Preflight validation check failed to guard for the given schema') + } +} +// ------------------------------------------------------------------ +// Policy +// ------------------------------------------------------------------ +export namespace Policy { + export function IsExactOptionalProperty(value: string, key: string, expression: string) { + return TypeSystemPolicy.ExactOptionalPropertyTypes ? `('${key}' in ${value} ? ${expression} : true)` : `(${MemberExpression.Encode(value, key)} !== undefined ? ${expression} : true)` + } + export function IsObjectLike(value: string): string { + return !TypeSystemPolicy.AllowArrayObject ? `(typeof ${value} === 'object' && ${value} !== null && !Array.isArray(${value}))` : `(typeof ${value} === 'object' && ${value} !== null)` + } + export function IsRecordLike(value: string): string { + return !TypeSystemPolicy.AllowArrayObject + ? `(typeof ${value} === 'object' && ${value} !== null && !Array.isArray(${value}) && !(${value} instanceof Date) && !(${value} instanceof Uint8Array))` + : `(typeof ${value} === 'object' && ${value} !== null && !(${value} instanceof Date) && !(${value} instanceof Uint8Array))` + } + export function IsNumberLike(value: string): string { + return TypeSystemPolicy.AllowNaN ? `typeof ${value} === 'number'` : `Number.isFinite(${value})` + } + export function IsVoidLike(value: string): string { + return TypeSystemPolicy.AllowNullVoid ? `(${value} === undefined || ${value} === null)` : `${value} === undefined` + } +} +// ------------------------------------------------------------------ +// TypeCompiler +// ------------------------------------------------------------------ +export type TypeCompilerLanguageOption = 'typescript' | 'javascript' +export interface TypeCompilerCodegenOptions { + language?: TypeCompilerLanguageOption +} +/** Compiles Types for Runtime Type Checking */ +export namespace TypeCompiler { + // ---------------------------------------------------------------- + // Guards + // ---------------------------------------------------------------- + function IsAnyOrUnknown(schema: TSchema) { + return schema[Kind] === 'Any' || schema[Kind] === 'Unknown' + } + // ---------------------------------------------------------------- + // Types + // ---------------------------------------------------------------- + function* FromAny(schema: TAny, references: TSchema[], value: string): IterableIterator { + yield 'true' + } + function* FromArgument(schema: TArgument, references: TSchema[], value: string): IterableIterator { + yield 'true' + } + function* FromArray(schema: TArray, references: TSchema[], value: string): IterableIterator { + yield `Array.isArray(${value})` + const [parameter, accumulator] = [CreateParameter('value', 'any'), CreateParameter('acc', 'number')] + if (IsNumber(schema.maxItems)) yield `${value}.length <= ${schema.maxItems}` + if (IsNumber(schema.minItems)) yield `${value}.length >= ${schema.minItems}` + const elementExpression = CreateExpression(schema.items, references, 'value') + yield `${value}.every((${parameter}) => ${elementExpression})` + if (IsSchema(schema.contains) || IsNumber(schema.minContains) || IsNumber(schema.maxContains)) { + const containsSchema = IsSchema(schema.contains) ? schema.contains : Never() + const checkExpression = CreateExpression(containsSchema, references, 'value') + const checkMinContains = IsNumber(schema.minContains) ? [`(count >= ${schema.minContains})`] : [] + const checkMaxContains = IsNumber(schema.maxContains) ? [`(count <= ${schema.maxContains})`] : [] + const checkCount = `const count = value.reduce((${accumulator}, ${parameter}) => ${checkExpression} ? acc + 1 : acc, 0)` + const check = [`(count > 0)`, ...checkMinContains, ...checkMaxContains].join(' && ') + yield `((${parameter}) => { ${checkCount}; return ${check}})(${value})` + } + if (schema.uniqueItems === true) { + const check = `const hashed = hash(element); if(set.has(hashed)) { return false } else { set.add(hashed) } } return true` + const block = `const set = new Set(); for(const element of value) { ${check} }` + yield `((${parameter}) => { ${block} )(${value})` + } + } + function* FromAsyncIterator(schema: TAsyncIterator, references: TSchema[], value: string): IterableIterator { + yield `(typeof value === 'object' && Symbol.asyncIterator in ${value})` + } + function* FromBigInt(schema: TBigInt, references: TSchema[], value: string): IterableIterator { + yield `(typeof ${value} === 'bigint')` + if (IsBigInt(schema.exclusiveMaximum)) yield `${value} < BigInt(${schema.exclusiveMaximum})` + if (IsBigInt(schema.exclusiveMinimum)) yield `${value} > BigInt(${schema.exclusiveMinimum})` + if (IsBigInt(schema.maximum)) yield `${value} <= BigInt(${schema.maximum})` + if (IsBigInt(schema.minimum)) yield `${value} >= BigInt(${schema.minimum})` + if (IsBigInt(schema.multipleOf)) yield `(${value} % BigInt(${schema.multipleOf})) === 0` + } + function* FromBoolean(schema: TBoolean, references: TSchema[], value: string): IterableIterator { + yield `(typeof ${value} === 'boolean')` + } + function* FromConstructor(schema: TConstructor, references: TSchema[], value: string): IterableIterator { + yield* Visit(schema.returns, references, `${value}.prototype`) + } + function* FromDate(schema: TDate, references: TSchema[], value: string): IterableIterator { + yield `(${value} instanceof Date) && Number.isFinite(${value}.getTime())` + if (IsNumber(schema.exclusiveMaximumTimestamp)) yield `${value}.getTime() < ${schema.exclusiveMaximumTimestamp}` + if (IsNumber(schema.exclusiveMinimumTimestamp)) yield `${value}.getTime() > ${schema.exclusiveMinimumTimestamp}` + if (IsNumber(schema.maximumTimestamp)) yield `${value}.getTime() <= ${schema.maximumTimestamp}` + if (IsNumber(schema.minimumTimestamp)) yield `${value}.getTime() >= ${schema.minimumTimestamp}` + if (IsNumber(schema.multipleOfTimestamp)) yield `(${value}.getTime() % ${schema.multipleOfTimestamp}) === 0` + } + function* FromFunction(schema: TFunction, references: TSchema[], value: string): IterableIterator { + yield `(typeof ${value} === 'function')` + } + function* FromImport(schema: TImport, references: TSchema[], value: string): IterableIterator { + const members = globalThis.Object.getOwnPropertyNames(schema.$defs).reduce((result, key) => { + return [...result, schema.$defs[key as never] as TSchema] + }, [] as TSchema[]) + yield* Visit(Ref(schema.$ref), [...references, ...members], value) + } + function* FromInteger(schema: TInteger, references: TSchema[], value: string): IterableIterator { + yield `Number.isInteger(${value})` + if (IsNumber(schema.exclusiveMaximum)) yield `${value} < ${schema.exclusiveMaximum}` + if (IsNumber(schema.exclusiveMinimum)) yield `${value} > ${schema.exclusiveMinimum}` + if (IsNumber(schema.maximum)) yield `${value} <= ${schema.maximum}` + if (IsNumber(schema.minimum)) yield `${value} >= ${schema.minimum}` + if (IsNumber(schema.multipleOf)) yield `(${value} % ${schema.multipleOf}) === 0` + } + function* FromIntersect(schema: TIntersect, references: TSchema[], value: string): IterableIterator { + const check1 = schema.allOf.map((schema: TSchema) => CreateExpression(schema, references, value)).join(' && ') + if (schema.unevaluatedProperties === false) { + const keyCheck = CreateVariable(`${new RegExp(KeyOfPattern(schema))};`) + const check2 = `Object.getOwnPropertyNames(${value}).every(key => ${keyCheck}.test(key))` + yield `(${check1} && ${check2})` + } else if (IsSchema(schema.unevaluatedProperties)) { + const keyCheck = CreateVariable(`${new RegExp(KeyOfPattern(schema))};`) + const check2 = `Object.getOwnPropertyNames(${value}).every(key => ${keyCheck}.test(key) || ${CreateExpression(schema.unevaluatedProperties, references, `${value}[key]`)})` + yield `(${check1} && ${check2})` + } else { + yield `(${check1})` + } + } + function* FromIterator(schema: TIterator, references: TSchema[], value: string): IterableIterator { + yield `(typeof value === 'object' && Symbol.iterator in ${value})` + } + function* FromLiteral(schema: TLiteral, references: TSchema[], value: string): IterableIterator { + if (typeof schema.const === 'number' || typeof schema.const === 'boolean') { + yield `(${value} === ${schema.const})` + } else { + yield `(${value} === '${LiteralString.Escape(schema.const)}')` + } + } + function* FromNever(schema: TNever, references: TSchema[], value: string): IterableIterator { + yield `false` + } + function* FromNot(schema: TNot, references: TSchema[], value: string): IterableIterator { + const expression = CreateExpression(schema.not, references, value) + yield `(!${expression})` + } + function* FromNull(schema: TNull, references: TSchema[], value: string): IterableIterator { + yield `(${value} === null)` + } + function* FromNumber(schema: TNumber, references: TSchema[], value: string): IterableIterator { + yield Policy.IsNumberLike(value) + if (IsNumber(schema.exclusiveMaximum)) yield `${value} < ${schema.exclusiveMaximum}` + if (IsNumber(schema.exclusiveMinimum)) yield `${value} > ${schema.exclusiveMinimum}` + if (IsNumber(schema.maximum)) yield `${value} <= ${schema.maximum}` + if (IsNumber(schema.minimum)) yield `${value} >= ${schema.minimum}` + if (IsNumber(schema.multipleOf)) yield `(${value} % ${schema.multipleOf}) === 0` + } + function* FromObject(schema: TObject, references: TSchema[], value: string): IterableIterator { + yield Policy.IsObjectLike(value) + if (IsNumber(schema.minProperties)) yield `Object.getOwnPropertyNames(${value}).length >= ${schema.minProperties}` + if (IsNumber(schema.maxProperties)) yield `Object.getOwnPropertyNames(${value}).length <= ${schema.maxProperties}` + const knownKeys = Object.getOwnPropertyNames(schema.properties) + for (const knownKey of knownKeys) { + const memberExpression = MemberExpression.Encode(value, knownKey) + const property = schema.properties[knownKey] + if (schema.required && schema.required.includes(knownKey)) { + yield* Visit(property, references, memberExpression) + if (ExtendsUndefinedCheck(property) || IsAnyOrUnknown(property)) yield `('${knownKey}' in ${value})` + } else { + const expression = CreateExpression(property, references, memberExpression) + yield Policy.IsExactOptionalProperty(value, knownKey, expression) + } + } + if (schema.additionalProperties === false) { + if (schema.required && schema.required.length === knownKeys.length) { + yield `Object.getOwnPropertyNames(${value}).length === ${knownKeys.length}` + } else { + const keys = `[${knownKeys.map((key) => `'${key}'`).join(', ')}]` + yield `Object.getOwnPropertyNames(${value}).every(key => ${keys}.includes(key))` + } + } + if (typeof schema.additionalProperties === 'object') { + const expression = CreateExpression(schema.additionalProperties, references, `${value}[key]`) + const keys = `[${knownKeys.map((key) => `'${key}'`).join(', ')}]` + yield `(Object.getOwnPropertyNames(${value}).every(key => ${keys}.includes(key) || ${expression}))` + } + } + function* FromPromise(schema: TPromise, references: TSchema[], value: string): IterableIterator { + yield `${value} instanceof Promise` + } + function* FromRecord(schema: TRecord, references: TSchema[], value: string): IterableIterator { + yield Policy.IsRecordLike(value) + if (IsNumber(schema.minProperties)) yield `Object.getOwnPropertyNames(${value}).length >= ${schema.minProperties}` + if (IsNumber(schema.maxProperties)) yield `Object.getOwnPropertyNames(${value}).length <= ${schema.maxProperties}` + const [patternKey, patternSchema] = Object.entries(schema.patternProperties)[0] + const variable = CreateVariable(`${new RegExp(patternKey)}`) + const check1 = CreateExpression(patternSchema, references, 'value') + const check2 = IsSchema(schema.additionalProperties) ? CreateExpression(schema.additionalProperties, references, value) : schema.additionalProperties === false ? 'false' : 'true' + const expression = `(${variable}.test(key) ? ${check1} : ${check2})` + yield `(Object.entries(${value}).every(([key, value]) => ${expression}))` + } + function* FromRef(schema: TRef, references: TSchema[], value: string): IterableIterator { + const target = Deref(schema, references) + // Reference: If we have seen this reference before we can just yield and return the function call. + // If this isn't the case we defer to visit to generate and set the function for subsequent passes. + if (state.functions.has(schema.$ref)) return yield `${CreateFunctionName(schema.$ref)}(${value})` + yield* Visit(target, references, value) + } + function* FromRegExp(schema: TRegExp, references: TSchema[], value: string): IterableIterator { + const variable = CreateVariable(`${new RegExp(schema.source, schema.flags)};`) + yield `(typeof ${value} === 'string')` + if (IsNumber(schema.maxLength)) yield `${value}.length <= ${schema.maxLength}` + if (IsNumber(schema.minLength)) yield `${value}.length >= ${schema.minLength}` + yield `${variable}.test(${value})` + } + function* FromString(schema: TString, references: TSchema[], value: string): IterableIterator { + yield `(typeof ${value} === 'string')` + if (IsNumber(schema.maxLength)) yield `${value}.length <= ${schema.maxLength}` + if (IsNumber(schema.minLength)) yield `${value}.length >= ${schema.minLength}` + if (schema.pattern !== undefined) { + const variable = CreateVariable(`${new RegExp(schema.pattern)};`) + yield `${variable}.test(${value})` + } + if (schema.format !== undefined) { + yield `format('${schema.format}', ${value})` + } + } + function* FromSymbol(schema: TSymbol, references: TSchema[], value: string): IterableIterator { + yield `(typeof ${value} === 'symbol')` + } + function* FromTemplateLiteral(schema: TTemplateLiteral, references: TSchema[], value: string): IterableIterator { + yield `(typeof ${value} === 'string')` + const variable = CreateVariable(`${new RegExp(schema.pattern)};`) + yield `${variable}.test(${value})` + } + function* FromThis(schema: TThis, references: TSchema[], value: string): IterableIterator { + // Note: This types are assured to be hoisted prior to this call. Just yield the function. + yield `${CreateFunctionName(schema.$ref)}(${value})` + } + function* FromTuple(schema: TTuple, references: TSchema[], value: string): IterableIterator { + yield `Array.isArray(${value})` + if (schema.items === undefined) return yield `${value}.length === 0` + yield `(${value}.length === ${schema.maxItems})` + for (let i = 0; i < schema.items.length; i++) { + const expression = CreateExpression(schema.items[i], references, `${value}[${i}]`) + yield `${expression}` + } + } + function* FromUndefined(schema: TUndefined, references: TSchema[], value: string): IterableIterator { + yield `${value} === undefined` + } + function* FromUnion(schema: TUnion, references: TSchema[], value: string): IterableIterator { + const expressions = schema.anyOf.map((schema: TSchema) => CreateExpression(schema, references, value)) + yield `(${expressions.join(' || ')})` + } + function* FromUint8Array(schema: TUint8Array, references: TSchema[], value: string): IterableIterator { + yield `${value} instanceof Uint8Array` + if (IsNumber(schema.maxByteLength)) yield `(${value}.length <= ${schema.maxByteLength})` + if (IsNumber(schema.minByteLength)) yield `(${value}.length >= ${schema.minByteLength})` + } + function* FromUnknown(schema: TUnknown, references: TSchema[], value: string): IterableIterator { + yield 'true' + } + function* FromVoid(schema: TVoid, references: TSchema[], value: string): IterableIterator { + yield Policy.IsVoidLike(value) + } + function* FromKind(schema: TSchema, references: TSchema[], value: string): IterableIterator { + const instance = state.instances.size + state.instances.set(instance, schema) + yield `kind('${schema[Kind]}', ${instance}, ${value})` + } + function* Visit(schema: TSchema, references: TSchema[], value: string, useHoisting: boolean = true): IterableIterator { + const references_ = IsString(schema.$id) ? [...references, schema] : references + const schema_ = schema as any + // -------------------------------------------------------------- + // Hoisting + // -------------------------------------------------------------- + if (useHoisting && IsString(schema.$id)) { + const functionName = CreateFunctionName(schema.$id) + if (state.functions.has(functionName)) { + return yield `${functionName}(${value})` + } else { + // Note: In the case of cyclic types, we need to create a 'functions' record + // to prevent infinitely re-visiting the CreateFunction. Subsequent attempts + // to visit will be caught by the above condition. + state.functions.set(functionName, '') + const functionCode = CreateFunction(functionName, schema, references, 'value', false) + state.functions.set(functionName, functionCode) + return yield `${functionName}(${value})` + } + } + switch (schema_[Kind]) { + case 'Any': + return yield* FromAny(schema_, references_, value) + case 'Argument': + return yield* FromArgument(schema_, references_, value) + case 'Array': + return yield* FromArray(schema_, references_, value) + case 'AsyncIterator': + return yield* FromAsyncIterator(schema_, references_, value) + case 'BigInt': + return yield* FromBigInt(schema_, references_, value) + case 'Boolean': + return yield* FromBoolean(schema_, references_, value) + case 'Constructor': + return yield* FromConstructor(schema_, references_, value) + case 'Date': + return yield* FromDate(schema_, references_, value) + case 'Function': + return yield* FromFunction(schema_, references_, value) + case 'Import': + return yield* FromImport(schema_, references_, value) + case 'Integer': + return yield* FromInteger(schema_, references_, value) + case 'Intersect': + return yield* FromIntersect(schema_, references_, value) + case 'Iterator': + return yield* FromIterator(schema_, references_, value) + case 'Literal': + return yield* FromLiteral(schema_, references_, value) + case 'Never': + return yield* FromNever(schema_, references_, value) + case 'Not': + return yield* FromNot(schema_, references_, value) + case 'Null': + return yield* FromNull(schema_, references_, value) + case 'Number': + return yield* FromNumber(schema_, references_, value) + case 'Object': + return yield* FromObject(schema_, references_, value) + case 'Promise': + return yield* FromPromise(schema_, references_, value) + case 'Record': + return yield* FromRecord(schema_, references_, value) + case 'Ref': + return yield* FromRef(schema_, references_, value) + case 'RegExp': + return yield* FromRegExp(schema_, references_, value) + case 'String': + return yield* FromString(schema_, references_, value) + case 'Symbol': + return yield* FromSymbol(schema_, references_, value) + case 'TemplateLiteral': + return yield* FromTemplateLiteral(schema_, references_, value) + case 'This': + return yield* FromThis(schema_, references_, value) + case 'Tuple': + return yield* FromTuple(schema_, references_, value) + case 'Undefined': + return yield* FromUndefined(schema_, references_, value) + case 'Union': + return yield* FromUnion(schema_, references_, value) + case 'Uint8Array': + return yield* FromUint8Array(schema_, references_, value) + case 'Unknown': + return yield* FromUnknown(schema_, references_, value) + case 'Void': + return yield* FromVoid(schema_, references_, value) + default: + if (!TypeRegistry.Has(schema_[Kind])) throw new TypeCompilerUnknownTypeError(schema) + return yield* FromKind(schema_, references_, value) + } + } + // ---------------------------------------------------------------- + // Compiler State + // ---------------------------------------------------------------- + // prettier-ignore + const state = { + language: 'javascript', // target language + functions: new Map(), // local functions + variables: new Map(), // local variables + instances: new Map() // exterior kind instances + } + // ---------------------------------------------------------------- + // Compiler Factory + // ---------------------------------------------------------------- + function CreateExpression(schema: TSchema, references: TSchema[], value: string, useHoisting: boolean = true): string { + return `(${[...Visit(schema, references, value, useHoisting)].join(' && ')})` + } + function CreateFunctionName($id: string) { + return `check_${Identifier.Encode($id)}` + } + function CreateVariable(expression: string) { + const variableName = `local_${state.variables.size}` + state.variables.set(variableName, `const ${variableName} = ${expression}`) + return variableName + } + function CreateFunction(name: string, schema: TSchema, references: TSchema[], value: string, useHoisting: boolean = true): string { + const [newline, pad] = ['\n', (length: number) => ''.padStart(length, ' ')] + const parameter = CreateParameter('value', 'any') + const returns = CreateReturns('boolean') + const expression = [...Visit(schema, references, value, useHoisting)].map((expression) => `${pad(4)}${expression}`).join(` &&${newline}`) + return `function ${name}(${parameter})${returns} {${newline}${pad(2)}return (${newline}${expression}${newline}${pad(2)})\n}` + } + function CreateParameter(name: string, type: string) { + const annotation = state.language === 'typescript' ? `: ${type}` : '' + return `${name}${annotation}` + } + function CreateReturns(type: string) { + return state.language === 'typescript' ? `: ${type}` : '' + } + // ---------------------------------------------------------------- + // Compile + // ---------------------------------------------------------------- + function Build(schema: T, references: TSchema[], options: TypeCompilerCodegenOptions): string { + const functionCode = CreateFunction('check', schema, references, 'value') // will populate functions and variables + const parameter = CreateParameter('value', 'any') + const returns = CreateReturns('boolean') + const functions = [...state.functions.values()] + const variables = [...state.variables.values()] + // prettier-ignore + const checkFunction = IsString(schema.$id) // ensure top level schemas with $id's are hoisted + ? `return function check(${parameter})${returns} {\n return ${CreateFunctionName(schema.$id)}(value)\n}` + : `return ${functionCode}` + return [...variables, ...functions, checkFunction].join('\n') + } + /** Generates the code used to assert this type and returns it as a string */ + export function Code(schema: T, references: TSchema[], options?: TypeCompilerCodegenOptions): string + /** Generates the code used to assert this type and returns it as a string */ + export function Code(schema: T, options?: TypeCompilerCodegenOptions): string + /** Generates the code used to assert this type and returns it as a string */ + export function Code(...args: any[]) { + const defaults = { language: 'javascript' } + // prettier-ignore + const [schema, references, options] = ( + args.length === 2 && IsArray(args[1]) ? [args[0], args[1], defaults] : + args.length === 2 && !IsArray(args[1]) ? [args[0], [], args[1]] : + args.length === 3 ? [args[0], args[1], args[2]] : + args.length === 1 ? [args[0], [], defaults] : + [null, [], defaults] + ) + // compiler-reset + state.language = options.language + state.variables.clear() + state.functions.clear() + state.instances.clear() + if (!IsSchema(schema)) throw new TypeCompilerTypeGuardError(schema) + for (const schema of references) if (!IsSchema(schema)) throw new TypeCompilerTypeGuardError(schema) + return Build(schema, references, options) + } + /** Compiles a TypeBox type for optimal runtime type checking. Types must be valid TypeBox types of TSchema */ + export function Compile(schema: T, references: TSchema[] = []): TypeCheck { + const generatedCode = Code(schema, references, { language: 'javascript' }) + const compiledFunction = globalThis.Function('kind', 'format', 'hash', generatedCode) + const instances = new Map(state.instances) + function typeRegistryFunction(kind: string, instance: number, value: unknown) { + if (!TypeRegistry.Has(kind) || !instances.has(instance)) return false + const checkFunc = TypeRegistry.Get(kind)! + const schema = instances.get(instance)! + return checkFunc(schema, value) + } + function formatRegistryFunction(format: string, value: string) { + if (!FormatRegistry.Has(format)) return false + const checkFunc = FormatRegistry.Get(format)! + return checkFunc(value) + } + function hashFunction(value: unknown) { + return Hash(value) + } + const checkFunction = compiledFunction(typeRegistryFunction, formatRegistryFunction, hashFunction) + return new TypeCheck(schema, references, checkFunction, generatedCode) + } +} diff --git a/src/compiler/index.ts b/src/compiler/index.ts new file mode 100644 index 000000000..4bdcefef7 --- /dev/null +++ b/src/compiler/index.ts @@ -0,0 +1,30 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/compiler + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export { ValueError, ValueErrorType, ValueErrorIterator } from '../errors/index' +export * from './compiler' diff --git a/src/errors/errors.ts b/src/errors/errors.ts new file mode 100644 index 000000000..87ef5e904 --- /dev/null +++ b/src/errors/errors.ts @@ -0,0 +1,639 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/errors + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { TypeSystemPolicy } from '../system/index' +import { KeyOfPattern } from '../type/keyof/index' +import { TypeRegistry, FormatRegistry } from '../type/registry/index' +import { ExtendsUndefinedCheck } from '../type/extends/extends-undefined' +import { GetErrorFunction } from './function' +import { TypeBoxError } from '../type/error/index' +import { Deref } from '../value/deref/index' +import { Hash } from '../value/hash/index' +import { Check } from '../value/check/index' +import { Kind } from '../type/symbols/index' + +import type { TSchema } from '../type/schema/index' +import type { TAsyncIterator } from '../type/async-iterator/index' +import type { TAny } from '../type/any/index' +import type { TArray } from '../type/array/index' +import type { TBigInt } from '../type/bigint/index' +import type { TBoolean } from '../type/boolean/index' +import type { TDate } from '../type/date/index' +import type { TConstructor } from '../type/constructor/index' +import type { TFunction } from '../type/function/index' +import type { TImport } from '../type/module/index' +import type { TInteger } from '../type/integer/index' +import type { TIntersect } from '../type/intersect/index' +import type { TIterator } from '../type/iterator/index' +import type { TLiteral } from '../type/literal/index' +import { Never, type TNever } from '../type/never/index' +import type { TNot } from '../type/not/index' +import type { TNull } from '../type/null/index' +import type { TNumber } from '../type/number/index' +import type { TObject } from '../type/object/index' +import type { TPromise } from '../type/promise/index' +import type { TRecord } from '../type/record/index' +import type { TRef } from '../type/ref/index' +import type { TRegExp } from '../type/regexp/index' +import type { TTemplateLiteral } from '../type/template-literal/index' +import type { TThis } from '../type/recursive/index' +import type { TTuple } from '../type/tuple/index' +import type { TUnion } from '../type/union/index' +import type { TUnknown } from '../type/unknown/index' +import type { TString } from '../type/string/index' +import type { TSymbol } from '../type/symbol/index' +import type { TUndefined } from '../type/undefined/index' +import type { TUint8Array } from '../type/uint8array/index' +import type { TVoid } from '../type/void/index' +// ------------------------------------------------------------------ +// ValueGuard +// ------------------------------------------------------------------ +// prettier-ignore +import { + IsArray, + IsUint8Array, + IsDate, + IsPromise, + IsFunction, + IsAsyncIterator, + IsIterator, + IsBoolean, + IsNumber, + IsBigInt, + IsString, + IsSymbol, + IsInteger, + IsNull, + IsUndefined +} from '../value/guard/index' +// ------------------------------------------------------------------ +// ValueErrorType +// ------------------------------------------------------------------ +export enum ValueErrorType { + ArrayContains, + ArrayMaxContains, + ArrayMaxItems, + ArrayMinContains, + ArrayMinItems, + ArrayUniqueItems, + Array, + AsyncIterator, + BigIntExclusiveMaximum, + BigIntExclusiveMinimum, + BigIntMaximum, + BigIntMinimum, + BigIntMultipleOf, + BigInt, + Boolean, + DateExclusiveMaximumTimestamp, + DateExclusiveMinimumTimestamp, + DateMaximumTimestamp, + DateMinimumTimestamp, + DateMultipleOfTimestamp, + Date, + Function, + IntegerExclusiveMaximum, + IntegerExclusiveMinimum, + IntegerMaximum, + IntegerMinimum, + IntegerMultipleOf, + Integer, + IntersectUnevaluatedProperties, + Intersect, + Iterator, + Kind, + Literal, + Never, + Not, + Null, + NumberExclusiveMaximum, + NumberExclusiveMinimum, + NumberMaximum, + NumberMinimum, + NumberMultipleOf, + Number, + ObjectAdditionalProperties, + ObjectMaxProperties, + ObjectMinProperties, + ObjectRequiredProperty, + Object, + Promise, + RegExp, + StringFormatUnknown, + StringFormat, + StringMaxLength, + StringMinLength, + StringPattern, + String, + Symbol, + TupleLength, + Tuple, + Uint8ArrayMaxByteLength, + Uint8ArrayMinByteLength, + Uint8Array, + Undefined, + Union, + Void, +} +// ------------------------------------------------------------------ +// ValueError +// ------------------------------------------------------------------ +export interface ValueError { + type: ValueErrorType + schema: TSchema + path: string + value: unknown + message: string + errors: ValueErrorIterator[] +} +// ------------------------------------------------------------------ +// ValueErrors +// ------------------------------------------------------------------ +export class ValueErrorsUnknownTypeError extends TypeBoxError { + constructor(public readonly schema: TSchema) { + super('Unknown type') + } +} +// ------------------------------------------------------------------ +// EscapeKey +// ------------------------------------------------------------------ +function EscapeKey(key: string): string { + return key.replace(/~/g, '~0').replace(/\//g, '~1') // RFC6901 Path +} +// ------------------------------------------------------------------ +// Guards +// ------------------------------------------------------------------ +function IsDefined(value: unknown): value is T { + return value !== undefined +} +// ------------------------------------------------------------------ +// ValueErrorIterator +// ------------------------------------------------------------------ +export class ValueErrorIterator { + constructor(private readonly iterator: IterableIterator) {} + public [Symbol.iterator]() { + return this.iterator + } + /** Returns the first value error or undefined if no errors */ + public First(): ValueError | undefined { + const next = this.iterator.next() + return next.done ? undefined : next.value + } +} +// -------------------------------------------------------------------------- +// Create +// -------------------------------------------------------------------------- +function Create(errorType: ValueErrorType, schema: TSchema, path: string, value: unknown, errors: ValueErrorIterator[] = []): ValueError { + return { + type: errorType, + schema, + path, + value, + message: GetErrorFunction()({ errorType, path, schema, value, errors }), + errors, + } +} +// -------------------------------------------------------------------------- +// Types +// -------------------------------------------------------------------------- +function* FromAny(schema: TAny, references: TSchema[], path: string, value: any): IterableIterator {} +function* FromArgument(schema: TAny, references: TSchema[], path: string, value: any): IterableIterator {} +function* FromArray(schema: TArray, references: TSchema[], path: string, value: any): IterableIterator { + if (!IsArray(value)) { + return yield Create(ValueErrorType.Array, schema, path, value) + } + if (IsDefined(schema.minItems) && !(value.length >= schema.minItems)) { + yield Create(ValueErrorType.ArrayMinItems, schema, path, value) + } + if (IsDefined(schema.maxItems) && !(value.length <= schema.maxItems)) { + yield Create(ValueErrorType.ArrayMaxItems, schema, path, value) + } + for (let i = 0; i < value.length; i++) { + yield* Visit(schema.items, references, `${path}/${i}`, value[i]) + } + // prettier-ignore + if (schema.uniqueItems === true && !((function () { const set = new Set(); for (const element of value) { const hashed = Hash(element); if (set.has(hashed)) { return false } else { set.add(hashed) } } return true })())) { + yield Create(ValueErrorType.ArrayUniqueItems, schema, path, value) + } + // contains + if (!(IsDefined(schema.contains) || IsDefined(schema.minContains) || IsDefined(schema.maxContains))) { + return + } + const containsSchema = IsDefined(schema.contains) ? schema.contains : Never() + const containsCount = value.reduce((acc: number, value, index) => (Visit(containsSchema, references, `${path}${index}`, value).next().done === true ? acc + 1 : acc), 0) + if (containsCount === 0) { + yield Create(ValueErrorType.ArrayContains, schema, path, value) + } + if (IsNumber(schema.minContains) && containsCount < schema.minContains) { + yield Create(ValueErrorType.ArrayMinContains, schema, path, value) + } + if (IsNumber(schema.maxContains) && containsCount > schema.maxContains) { + yield Create(ValueErrorType.ArrayMaxContains, schema, path, value) + } +} +function* FromAsyncIterator(schema: TAsyncIterator, references: TSchema[], path: string, value: any): IterableIterator { + if (!IsAsyncIterator(value)) yield Create(ValueErrorType.AsyncIterator, schema, path, value) +} +function* FromBigInt(schema: TBigInt, references: TSchema[], path: string, value: any): IterableIterator { + if (!IsBigInt(value)) return yield Create(ValueErrorType.BigInt, schema, path, value) + if (IsDefined(schema.exclusiveMaximum) && !(value < schema.exclusiveMaximum)) { + yield Create(ValueErrorType.BigIntExclusiveMaximum, schema, path, value) + } + if (IsDefined(schema.exclusiveMinimum) && !(value > schema.exclusiveMinimum)) { + yield Create(ValueErrorType.BigIntExclusiveMinimum, schema, path, value) + } + if (IsDefined(schema.maximum) && !(value <= schema.maximum)) { + yield Create(ValueErrorType.BigIntMaximum, schema, path, value) + } + if (IsDefined(schema.minimum) && !(value >= schema.minimum)) { + yield Create(ValueErrorType.BigIntMinimum, schema, path, value) + } + if (IsDefined(schema.multipleOf) && !(value % schema.multipleOf === BigInt(0))) { + yield Create(ValueErrorType.BigIntMultipleOf, schema, path, value) + } +} +function* FromBoolean(schema: TBoolean, references: TSchema[], path: string, value: any): IterableIterator { + if (!IsBoolean(value)) yield Create(ValueErrorType.Boolean, schema, path, value) +} +function* FromConstructor(schema: TConstructor, references: TSchema[], path: string, value: any): IterableIterator { + yield* Visit(schema.returns, references, path, value.prototype) +} +function* FromDate(schema: TDate, references: TSchema[], path: string, value: any): IterableIterator { + if (!IsDate(value)) return yield Create(ValueErrorType.Date, schema, path, value) + if (IsDefined(schema.exclusiveMaximumTimestamp) && !(value.getTime() < schema.exclusiveMaximumTimestamp)) { + yield Create(ValueErrorType.DateExclusiveMaximumTimestamp, schema, path, value) + } + if (IsDefined(schema.exclusiveMinimumTimestamp) && !(value.getTime() > schema.exclusiveMinimumTimestamp)) { + yield Create(ValueErrorType.DateExclusiveMinimumTimestamp, schema, path, value) + } + if (IsDefined(schema.maximumTimestamp) && !(value.getTime() <= schema.maximumTimestamp)) { + yield Create(ValueErrorType.DateMaximumTimestamp, schema, path, value) + } + if (IsDefined(schema.minimumTimestamp) && !(value.getTime() >= schema.minimumTimestamp)) { + yield Create(ValueErrorType.DateMinimumTimestamp, schema, path, value) + } + if (IsDefined(schema.multipleOfTimestamp) && !(value.getTime() % schema.multipleOfTimestamp === 0)) { + yield Create(ValueErrorType.DateMultipleOfTimestamp, schema, path, value) + } +} +function* FromFunction(schema: TFunction, references: TSchema[], path: string, value: any): IterableIterator { + if (!IsFunction(value)) yield Create(ValueErrorType.Function, schema, path, value) +} +function* FromImport(schema: TImport, references: TSchema[], path: string, value: any): IterableIterator { + const definitions = globalThis.Object.values(schema.$defs) as TSchema[] + const target = schema.$defs[schema.$ref] as TSchema + yield* Visit(target, [...references, ...definitions], path, value) +} +function* FromInteger(schema: TInteger, references: TSchema[], path: string, value: any): IterableIterator { + if (!IsInteger(value)) return yield Create(ValueErrorType.Integer, schema, path, value) + if (IsDefined(schema.exclusiveMaximum) && !(value < schema.exclusiveMaximum)) { + yield Create(ValueErrorType.IntegerExclusiveMaximum, schema, path, value) + } + if (IsDefined(schema.exclusiveMinimum) && !(value > schema.exclusiveMinimum)) { + yield Create(ValueErrorType.IntegerExclusiveMinimum, schema, path, value) + } + if (IsDefined(schema.maximum) && !(value <= schema.maximum)) { + yield Create(ValueErrorType.IntegerMaximum, schema, path, value) + } + if (IsDefined(schema.minimum) && !(value >= schema.minimum)) { + yield Create(ValueErrorType.IntegerMinimum, schema, path, value) + } + if (IsDefined(schema.multipleOf) && !(value % schema.multipleOf === 0)) { + yield Create(ValueErrorType.IntegerMultipleOf, schema, path, value) + } +} +function* FromIntersect(schema: TIntersect, references: TSchema[], path: string, value: any): IterableIterator { + let hasError = false + for (const inner of schema.allOf) { + for (const error of Visit(inner, references, path, value)) { + hasError = true + yield error + } + } + if (hasError) { + return yield Create(ValueErrorType.Intersect, schema, path, value) + } + if (schema.unevaluatedProperties === false) { + const keyCheck = new RegExp(KeyOfPattern(schema)) + for (const valueKey of Object.getOwnPropertyNames(value)) { + if (!keyCheck.test(valueKey)) { + yield Create(ValueErrorType.IntersectUnevaluatedProperties, schema, `${path}/${valueKey}`, value) + } + } + } + if (typeof schema.unevaluatedProperties === 'object') { + const keyCheck = new RegExp(KeyOfPattern(schema)) + for (const valueKey of Object.getOwnPropertyNames(value)) { + if (!keyCheck.test(valueKey)) { + const next = Visit(schema.unevaluatedProperties, references, `${path}/${valueKey}`, value[valueKey]).next() + if (!next.done) yield next.value // yield interior + } + } + } +} +function* FromIterator(schema: TIterator, references: TSchema[], path: string, value: any): IterableIterator { + if (!IsIterator(value)) yield Create(ValueErrorType.Iterator, schema, path, value) +} +function* FromLiteral(schema: TLiteral, references: TSchema[], path: string, value: any): IterableIterator { + if (!(value === schema.const)) yield Create(ValueErrorType.Literal, schema, path, value) +} +function* FromNever(schema: TNever, references: TSchema[], path: string, value: any): IterableIterator { + yield Create(ValueErrorType.Never, schema, path, value) +} +function* FromNot(schema: TNot, references: TSchema[], path: string, value: any): IterableIterator { + if (Visit(schema.not, references, path, value).next().done === true) yield Create(ValueErrorType.Not, schema, path, value) +} +function* FromNull(schema: TNull, references: TSchema[], path: string, value: any): IterableIterator { + if (!IsNull(value)) yield Create(ValueErrorType.Null, schema, path, value) +} +function* FromNumber(schema: TNumber, references: TSchema[], path: string, value: any): IterableIterator { + if (!TypeSystemPolicy.IsNumberLike(value)) return yield Create(ValueErrorType.Number, schema, path, value) + if (IsDefined(schema.exclusiveMaximum) && !(value < schema.exclusiveMaximum)) { + yield Create(ValueErrorType.NumberExclusiveMaximum, schema, path, value) + } + if (IsDefined(schema.exclusiveMinimum) && !(value > schema.exclusiveMinimum)) { + yield Create(ValueErrorType.NumberExclusiveMinimum, schema, path, value) + } + if (IsDefined(schema.maximum) && !(value <= schema.maximum)) { + yield Create(ValueErrorType.NumberMaximum, schema, path, value) + } + if (IsDefined(schema.minimum) && !(value >= schema.minimum)) { + yield Create(ValueErrorType.NumberMinimum, schema, path, value) + } + if (IsDefined(schema.multipleOf) && !(value % schema.multipleOf === 0)) { + yield Create(ValueErrorType.NumberMultipleOf, schema, path, value) + } +} +function* FromObject(schema: TObject, references: TSchema[], path: string, value: any): IterableIterator { + if (!TypeSystemPolicy.IsObjectLike(value)) return yield Create(ValueErrorType.Object, schema, path, value) + if (IsDefined(schema.minProperties) && !(Object.getOwnPropertyNames(value).length >= schema.minProperties)) { + yield Create(ValueErrorType.ObjectMinProperties, schema, path, value) + } + if (IsDefined(schema.maxProperties) && !(Object.getOwnPropertyNames(value).length <= schema.maxProperties)) { + yield Create(ValueErrorType.ObjectMaxProperties, schema, path, value) + } + const requiredKeys = Array.isArray(schema.required) ? schema.required : ([] as string[]) + const knownKeys = Object.getOwnPropertyNames(schema.properties) + const unknownKeys = Object.getOwnPropertyNames(value) + for (const requiredKey of requiredKeys) { + if (unknownKeys.includes(requiredKey)) continue + yield Create(ValueErrorType.ObjectRequiredProperty, schema.properties[requiredKey], `${path}/${EscapeKey(requiredKey)}`, undefined) + } + if (schema.additionalProperties === false) { + for (const valueKey of unknownKeys) { + if (!knownKeys.includes(valueKey)) { + yield Create(ValueErrorType.ObjectAdditionalProperties, schema, `${path}/${EscapeKey(valueKey)}`, value[valueKey]) + } + } + } + if (typeof schema.additionalProperties === 'object') { + for (const valueKey of unknownKeys) { + if (knownKeys.includes(valueKey)) continue + yield* Visit(schema.additionalProperties as TSchema, references, `${path}/${EscapeKey(valueKey)}`, value[valueKey]) + } + } + for (const knownKey of knownKeys) { + const property = schema.properties[knownKey] + if (schema.required && schema.required.includes(knownKey)) { + yield* Visit(property, references, `${path}/${EscapeKey(knownKey)}`, value[knownKey]) + if (ExtendsUndefinedCheck(schema) && !(knownKey in value)) { + yield Create(ValueErrorType.ObjectRequiredProperty, property, `${path}/${EscapeKey(knownKey)}`, undefined) + } + } else { + if (TypeSystemPolicy.IsExactOptionalProperty(value, knownKey)) { + yield* Visit(property, references, `${path}/${EscapeKey(knownKey)}`, value[knownKey]) + } + } + } +} +function* FromPromise(schema: TPromise, references: TSchema[], path: string, value: any): IterableIterator { + if (!IsPromise(value)) yield Create(ValueErrorType.Promise, schema, path, value) +} +function* FromRecord(schema: TRecord, references: TSchema[], path: string, value: any): IterableIterator { + if (!TypeSystemPolicy.IsRecordLike(value)) return yield Create(ValueErrorType.Object, schema, path, value) + if (IsDefined(schema.minProperties) && !(Object.getOwnPropertyNames(value).length >= schema.minProperties)) { + yield Create(ValueErrorType.ObjectMinProperties, schema, path, value) + } + if (IsDefined(schema.maxProperties) && !(Object.getOwnPropertyNames(value).length <= schema.maxProperties)) { + yield Create(ValueErrorType.ObjectMaxProperties, schema, path, value) + } + const [patternKey, patternSchema] = Object.entries(schema.patternProperties)[0] + const regex = new RegExp(patternKey) + for (const [propertyKey, propertyValue] of Object.entries(value)) { + if (regex.test(propertyKey)) yield* Visit(patternSchema, references, `${path}/${EscapeKey(propertyKey)}`, propertyValue) + } + if (typeof schema.additionalProperties === 'object') { + for (const [propertyKey, propertyValue] of Object.entries(value)) { + if (!regex.test(propertyKey)) yield* Visit(schema.additionalProperties as TSchema, references, `${path}/${EscapeKey(propertyKey)}`, propertyValue) + } + } + if (schema.additionalProperties === false) { + for (const [propertyKey, propertyValue] of Object.entries(value)) { + if (regex.test(propertyKey)) continue + return yield Create(ValueErrorType.ObjectAdditionalProperties, schema, `${path}/${EscapeKey(propertyKey)}`, propertyValue) + } + } +} +function* FromRef(schema: TRef, references: TSchema[], path: string, value: any): IterableIterator { + yield* Visit(Deref(schema, references), references, path, value) +} +function* FromRegExp(schema: TRegExp, references: TSchema[], path: string, value: any): IterableIterator { + if (!IsString(value)) return yield Create(ValueErrorType.String, schema, path, value) + if (IsDefined(schema.minLength) && !(value.length >= schema.minLength)) { + yield Create(ValueErrorType.StringMinLength, schema, path, value) + } + if (IsDefined(schema.maxLength) && !(value.length <= schema.maxLength)) { + yield Create(ValueErrorType.StringMaxLength, schema, path, value) + } + const regex = new RegExp(schema.source, schema.flags) + if (!regex.test(value)) { + return yield Create(ValueErrorType.RegExp, schema, path, value) + } +} +function* FromString(schema: TString, references: TSchema[], path: string, value: any): IterableIterator { + if (!IsString(value)) return yield Create(ValueErrorType.String, schema, path, value) + if (IsDefined(schema.minLength) && !(value.length >= schema.minLength)) { + yield Create(ValueErrorType.StringMinLength, schema, path, value) + } + if (IsDefined(schema.maxLength) && !(value.length <= schema.maxLength)) { + yield Create(ValueErrorType.StringMaxLength, schema, path, value) + } + if (IsString(schema.pattern)) { + const regex = new RegExp(schema.pattern) + if (!regex.test(value)) { + yield Create(ValueErrorType.StringPattern, schema, path, value) + } + } + if (IsString(schema.format)) { + if (!FormatRegistry.Has(schema.format)) { + yield Create(ValueErrorType.StringFormatUnknown, schema, path, value) + } else { + const format = FormatRegistry.Get(schema.format)! + if (!format(value)) { + yield Create(ValueErrorType.StringFormat, schema, path, value) + } + } + } +} +function* FromSymbol(schema: TSymbol, references: TSchema[], path: string, value: any): IterableIterator { + if (!IsSymbol(value)) yield Create(ValueErrorType.Symbol, schema, path, value) +} +function* FromTemplateLiteral(schema: TTemplateLiteral, references: TSchema[], path: string, value: any): IterableIterator { + if (!IsString(value)) return yield Create(ValueErrorType.String, schema, path, value) + const regex = new RegExp(schema.pattern) + if (!regex.test(value)) { + yield Create(ValueErrorType.StringPattern, schema, path, value) + } +} +function* FromThis(schema: TThis, references: TSchema[], path: string, value: any): IterableIterator { + yield* Visit(Deref(schema, references), references, path, value) +} +function* FromTuple(schema: TTuple, references: TSchema[], path: string, value: any): IterableIterator { + if (!IsArray(value)) return yield Create(ValueErrorType.Tuple, schema, path, value) + if (schema.items === undefined && !(value.length === 0)) { + return yield Create(ValueErrorType.TupleLength, schema, path, value) + } + if (!(value.length === schema.maxItems)) { + return yield Create(ValueErrorType.TupleLength, schema, path, value) + } + if (!schema.items) { + return + } + for (let i = 0; i < schema.items.length; i++) { + yield* Visit(schema.items[i], references, `${path}/${i}`, value[i]) + } +} +function* FromUndefined(schema: TUndefined, references: TSchema[], path: string, value: any): IterableIterator { + if (!IsUndefined(value)) yield Create(ValueErrorType.Undefined, schema, path, value) +} +function* FromUnion(schema: TUnion, references: TSchema[], path: string, value: any): IterableIterator { + if (Check(schema, references, value)) return + const errors = schema.anyOf.map((variant) => new ValueErrorIterator(Visit(variant, references, path, value))) + yield Create(ValueErrorType.Union, schema, path, value, errors) +} +function* FromUint8Array(schema: TUint8Array, references: TSchema[], path: string, value: any): IterableIterator { + if (!IsUint8Array(value)) return yield Create(ValueErrorType.Uint8Array, schema, path, value) + if (IsDefined(schema.maxByteLength) && !(value.length <= schema.maxByteLength)) { + yield Create(ValueErrorType.Uint8ArrayMaxByteLength, schema, path, value) + } + if (IsDefined(schema.minByteLength) && !(value.length >= schema.minByteLength)) { + yield Create(ValueErrorType.Uint8ArrayMinByteLength, schema, path, value) + } +} +function* FromUnknown(schema: TUnknown, references: TSchema[], path: string, value: any): IterableIterator {} +function* FromVoid(schema: TVoid, references: TSchema[], path: string, value: any): IterableIterator { + if (!TypeSystemPolicy.IsVoidLike(value)) yield Create(ValueErrorType.Void, schema, path, value) +} +function* FromKind(schema: TSchema, references: TSchema[], path: string, value: any): IterableIterator { + const check = TypeRegistry.Get(schema[Kind])! + if (!check(schema, value)) yield Create(ValueErrorType.Kind, schema, path, value) +} +function* Visit(schema: T, references: TSchema[], path: string, value: any): IterableIterator { + const references_ = IsDefined(schema.$id) ? [...references, schema] : references + const schema_ = schema as any + switch (schema_[Kind]) { + case 'Any': + return yield* FromAny(schema_, references_, path, value) + case 'Argument': + return yield* FromArgument(schema_, references_, path, value) + case 'Array': + return yield* FromArray(schema_, references_, path, value) + case 'AsyncIterator': + return yield* FromAsyncIterator(schema_, references_, path, value) + case 'BigInt': + return yield* FromBigInt(schema_, references_, path, value) + case 'Boolean': + return yield* FromBoolean(schema_, references_, path, value) + case 'Constructor': + return yield* FromConstructor(schema_, references_, path, value) + case 'Date': + return yield* FromDate(schema_, references_, path, value) + case 'Function': + return yield* FromFunction(schema_, references_, path, value) + case 'Import': + return yield* FromImport(schema_, references_, path, value) + case 'Integer': + return yield* FromInteger(schema_, references_, path, value) + case 'Intersect': + return yield* FromIntersect(schema_, references_, path, value) + case 'Iterator': + return yield* FromIterator(schema_, references_, path, value) + case 'Literal': + return yield* FromLiteral(schema_, references_, path, value) + case 'Never': + return yield* FromNever(schema_, references_, path, value) + case 'Not': + return yield* FromNot(schema_, references_, path, value) + case 'Null': + return yield* FromNull(schema_, references_, path, value) + case 'Number': + return yield* FromNumber(schema_, references_, path, value) + case 'Object': + return yield* FromObject(schema_, references_, path, value) + case 'Promise': + return yield* FromPromise(schema_, references_, path, value) + case 'Record': + return yield* FromRecord(schema_, references_, path, value) + case 'Ref': + return yield* FromRef(schema_, references_, path, value) + case 'RegExp': + return yield* FromRegExp(schema_, references_, path, value) + case 'String': + return yield* FromString(schema_, references_, path, value) + case 'Symbol': + return yield* FromSymbol(schema_, references_, path, value) + case 'TemplateLiteral': + return yield* FromTemplateLiteral(schema_, references_, path, value) + case 'This': + return yield* FromThis(schema_, references_, path, value) + case 'Tuple': + return yield* FromTuple(schema_, references_, path, value) + case 'Undefined': + return yield* FromUndefined(schema_, references_, path, value) + case 'Union': + return yield* FromUnion(schema_, references_, path, value) + case 'Uint8Array': + return yield* FromUint8Array(schema_, references_, path, value) + case 'Unknown': + return yield* FromUnknown(schema_, references_, path, value) + case 'Void': + return yield* FromVoid(schema_, references_, path, value) + default: + if (!TypeRegistry.Has(schema_[Kind])) throw new ValueErrorsUnknownTypeError(schema) + return yield* FromKind(schema_, references_, path, value) + } +} +/** Returns an iterator for each error in this value. */ +export function Errors(schema: T, references: TSchema[], value: unknown): ValueErrorIterator +/** Returns an iterator for each error in this value. */ +export function Errors(schema: T, value: unknown): ValueErrorIterator +/** Returns an iterator for each error in this value. */ +export function Errors(...args: any[]) { + const iterator = args.length === 3 ? Visit(args[0], args[1], '', args[2]) : Visit(args[0], [], '', args[1]) + return new ValueErrorIterator(iterator) +} diff --git a/src/errors/function.ts b/src/errors/function.ts new file mode 100644 index 000000000..39ac67220 --- /dev/null +++ b/src/errors/function.ts @@ -0,0 +1,195 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/system + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { TSchema } from '../type/schema/index' +import { Kind } from '../type/symbols/index' +import { ValueErrorIterator, ValueErrorType } from './errors' + +/** Creates an error message using en-US as the default locale */ +export function DefaultErrorFunction(error: ErrorFunctionParameter) { + switch (error.errorType) { + case ValueErrorType.ArrayContains: + return 'Expected array to contain at least one matching value' + case ValueErrorType.ArrayMaxContains: + return `Expected array to contain no more than ${error.schema.maxContains} matching values` + case ValueErrorType.ArrayMinContains: + return `Expected array to contain at least ${error.schema.minContains} matching values` + case ValueErrorType.ArrayMaxItems: + return `Expected array length to be less or equal to ${error.schema.maxItems}` + case ValueErrorType.ArrayMinItems: + return `Expected array length to be greater or equal to ${error.schema.minItems}` + case ValueErrorType.ArrayUniqueItems: + return 'Expected array elements to be unique' + case ValueErrorType.Array: + return 'Expected array' + case ValueErrorType.AsyncIterator: + return 'Expected AsyncIterator' + case ValueErrorType.BigIntExclusiveMaximum: + return `Expected bigint to be less than ${error.schema.exclusiveMaximum}` + case ValueErrorType.BigIntExclusiveMinimum: + return `Expected bigint to be greater than ${error.schema.exclusiveMinimum}` + case ValueErrorType.BigIntMaximum: + return `Expected bigint to be less or equal to ${error.schema.maximum}` + case ValueErrorType.BigIntMinimum: + return `Expected bigint to be greater or equal to ${error.schema.minimum}` + case ValueErrorType.BigIntMultipleOf: + return `Expected bigint to be a multiple of ${error.schema.multipleOf}` + case ValueErrorType.BigInt: + return 'Expected bigint' + case ValueErrorType.Boolean: + return 'Expected boolean' + case ValueErrorType.DateExclusiveMinimumTimestamp: + return `Expected Date timestamp to be greater than ${error.schema.exclusiveMinimumTimestamp}` + case ValueErrorType.DateExclusiveMaximumTimestamp: + return `Expected Date timestamp to be less than ${error.schema.exclusiveMaximumTimestamp}` + case ValueErrorType.DateMinimumTimestamp: + return `Expected Date timestamp to be greater or equal to ${error.schema.minimumTimestamp}` + case ValueErrorType.DateMaximumTimestamp: + return `Expected Date timestamp to be less or equal to ${error.schema.maximumTimestamp}` + case ValueErrorType.DateMultipleOfTimestamp: + return `Expected Date timestamp to be a multiple of ${error.schema.multipleOfTimestamp}` + case ValueErrorType.Date: + return 'Expected Date' + case ValueErrorType.Function: + return 'Expected function' + case ValueErrorType.IntegerExclusiveMaximum: + return `Expected integer to be less than ${error.schema.exclusiveMaximum}` + case ValueErrorType.IntegerExclusiveMinimum: + return `Expected integer to be greater than ${error.schema.exclusiveMinimum}` + case ValueErrorType.IntegerMaximum: + return `Expected integer to be less or equal to ${error.schema.maximum}` + case ValueErrorType.IntegerMinimum: + return `Expected integer to be greater or equal to ${error.schema.minimum}` + case ValueErrorType.IntegerMultipleOf: + return `Expected integer to be a multiple of ${error.schema.multipleOf}` + case ValueErrorType.Integer: + return 'Expected integer' + case ValueErrorType.IntersectUnevaluatedProperties: + return 'Unexpected property' + case ValueErrorType.Intersect: + return 'Expected all values to match' + case ValueErrorType.Iterator: + return 'Expected Iterator' + case ValueErrorType.Literal: + return `Expected ${typeof error.schema.const === 'string' ? `'${error.schema.const}'` : error.schema.const}` + case ValueErrorType.Never: + return 'Never' + case ValueErrorType.Not: + return 'Value should not match' + case ValueErrorType.Null: + return 'Expected null' + case ValueErrorType.NumberExclusiveMaximum: + return `Expected number to be less than ${error.schema.exclusiveMaximum}` + case ValueErrorType.NumberExclusiveMinimum: + return `Expected number to be greater than ${error.schema.exclusiveMinimum}` + case ValueErrorType.NumberMaximum: + return `Expected number to be less or equal to ${error.schema.maximum}` + case ValueErrorType.NumberMinimum: + return `Expected number to be greater or equal to ${error.schema.minimum}` + case ValueErrorType.NumberMultipleOf: + return `Expected number to be a multiple of ${error.schema.multipleOf}` + case ValueErrorType.Number: + return 'Expected number' + case ValueErrorType.Object: + return 'Expected object' + case ValueErrorType.ObjectAdditionalProperties: + return 'Unexpected property' + case ValueErrorType.ObjectMaxProperties: + return `Expected object to have no more than ${error.schema.maxProperties} properties` + case ValueErrorType.ObjectMinProperties: + return `Expected object to have at least ${error.schema.minProperties} properties` + case ValueErrorType.ObjectRequiredProperty: + return 'Expected required property' + case ValueErrorType.Promise: + return 'Expected Promise' + case ValueErrorType.RegExp: + return 'Expected string to match regular expression' + case ValueErrorType.StringFormatUnknown: + return `Unknown format '${error.schema.format}'` + case ValueErrorType.StringFormat: + return `Expected string to match '${error.schema.format}' format` + case ValueErrorType.StringMaxLength: + return `Expected string length less or equal to ${error.schema.maxLength}` + case ValueErrorType.StringMinLength: + return `Expected string length greater or equal to ${error.schema.minLength}` + case ValueErrorType.StringPattern: + return `Expected string to match '${error.schema.pattern}'` + case ValueErrorType.String: + return 'Expected string' + case ValueErrorType.Symbol: + return 'Expected symbol' + case ValueErrorType.TupleLength: + return `Expected tuple to have ${error.schema.maxItems || 0} elements` + case ValueErrorType.Tuple: + return 'Expected tuple' + case ValueErrorType.Uint8ArrayMaxByteLength: + return `Expected byte length less or equal to ${error.schema.maxByteLength}` + case ValueErrorType.Uint8ArrayMinByteLength: + return `Expected byte length greater or equal to ${error.schema.minByteLength}` + case ValueErrorType.Uint8Array: + return 'Expected Uint8Array' + case ValueErrorType.Undefined: + return 'Expected undefined' + case ValueErrorType.Union: + return 'Expected union value' + case ValueErrorType.Void: + return 'Expected void' + case ValueErrorType.Kind: + return `Expected kind '${error.schema[Kind]}'` + default: + return 'Unknown error type' + } +} + +// ------------------------------------------------------------------ +// ErrorFunction +// ------------------------------------------------------------------ +export type ErrorFunctionParameter = { + /** The type of validation error */ + errorType: ValueErrorType + /** The path of the error */ + path: string + /** The schema associated with the error */ + schema: TSchema + /** The value associated with the error */ + value: unknown + /** Interior errors for this error */ + errors: ValueErrorIterator[] +} +export type ErrorFunction = (parameter: ErrorFunctionParameter) => string +/** Manages error message providers */ +let errorFunction: ErrorFunction = DefaultErrorFunction + +/** Sets the error function used to generate error messages. */ +export function SetErrorFunction(callback: ErrorFunction) { + errorFunction = callback +} +/** Gets the error function used to generate error messages */ +export function GetErrorFunction(): ErrorFunction { + return errorFunction +} diff --git a/src/errors/index.ts b/src/errors/index.ts new file mode 100644 index 000000000..4c499cc76 --- /dev/null +++ b/src/errors/index.ts @@ -0,0 +1,30 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/errors + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './errors' +export * from './function' diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 000000000..54d6bbc8a --- /dev/null +++ b/src/index.ts @@ -0,0 +1,109 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +// ------------------------------------------------------------------ +// Infrastructure +// ------------------------------------------------------------------ +export * from './type/clone/index' +export * from './type/create/index' +export * from './type/error/index' +export * from './type/guard/index' +export * from './type/helpers/index' +export * from './type/patterns/index' +export * from './type/registry/index' +export * from './type/sets/index' +export * from './type/symbols/index' + +// ------------------------------------------------------------------ +// Types +// ------------------------------------------------------------------ +export * from './type/any/index' +export * from './type/array/index' +export * from './type/argument/index' +export * from './type/async-iterator/index' +export * from './type/awaited/index' +export * from './type/bigint/index' +export * from './type/boolean/index' +export * from './type/composite/index' +export * from './type/const/index' +export * from './type/constructor/index' +export * from './type/constructor-parameters/index' +export * from './type/date/index' +export * from './type/enum/index' +export * from './type/exclude/index' +export * from './type/extends/index' +export * from './type/extract/index' +export * from './type/function/index' +export * from './type/indexed/index' +export * from './type/instance-type/index' +export * from './type/instantiate/index' +export * from './type/integer/index' +export * from './type/intersect/index' +export * from './type/iterator/index' +export * from './type/intrinsic/index' +export * from './type/keyof/index' +export * from './type/literal/index' +export * from './type/module/index' +export * from './type/mapped/index' +export * from './type/never/index' +export * from './type/not/index' +export * from './type/null/index' +export * from './type/number/index' +export * from './type/object/index' +export * from './type/omit/index' +export * from './type/optional/index' +export * from './type/parameters/index' +export * from './type/partial/index' +export * from './type/pick/index' +export * from './type/promise/index' +export * from './type/readonly/index' +export * from './type/readonly-optional/index' +export * from './type/record/index' +export * from './type/recursive/index' +export * from './type/ref/index' +export * from './type/regexp/index' +export * from './type/required/index' +export * from './type/rest/index' +export * from './type/return-type/index' +export * from './type/schema/index' +export * from './type/static/index' +export * from './type/string/index' +export * from './type/symbol/index' +export * from './type/template-literal/index' +export * from './type/transform/index' +export * from './type/tuple/index' +export * from './type/uint8array/index' +export * from './type/undefined/index' +export * from './type/union/index' +export * from './type/unknown/index' +export * from './type/unsafe/index' +export * from './type/void/index' +// ------------------------------------------------------------------ +// Type.* +// ------------------------------------------------------------------ +export * from './type/type/index' diff --git a/src/parser/index.ts b/src/parser/index.ts new file mode 100644 index 000000000..ec3432ded --- /dev/null +++ b/src/parser/index.ts @@ -0,0 +1,30 @@ +/*-------------------------------------------------------------------------- + +@sinclair/parsebox + +The MIT License (MIT) + +Copyright (c) 2024 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * as Runtime from './runtime/index' +export * as Static from './static/index' diff --git a/src/parser/runtime/guard.ts b/src/parser/runtime/guard.ts new file mode 100644 index 000000000..a340b8e86 --- /dev/null +++ b/src/parser/runtime/guard.ts @@ -0,0 +1,104 @@ +/*-------------------------------------------------------------------------- + +@sinclair/parsebox + +The MIT License (MIT) + +Copyright (c) 2024 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { IArray, IConst, IContext, IIdent, INumber, IOptional, IRef, IString, ITuple, IUnion } from './types' + +// ------------------------------------------------------------------ +// Value Guard +// ------------------------------------------------------------------ +// prettier-ignore +function HasPropertyKey(value: Record, key: Key): value is Record & { [_ in Key]: unknown } { + return key in value +} +// prettier-ignore +function IsObjectValue(value: unknown): value is Record { + return typeof value === 'object' && value !== null +} +// prettier-ignore +function IsArrayValue(value: unknown): value is unknown[] { + return globalThis.Array.isArray(value) +} +// ------------------------------------------------------------------ +// Parser Guard +// ------------------------------------------------------------------ +/** Returns true if the value is a Array Parser */ +export function IsArray(value: unknown): value is IArray { + return IsObjectValue(value) && HasPropertyKey(value, 'type') && value.type === 'Array' && HasPropertyKey(value, 'parser') && IsObjectValue(value.parser) +} +/** Returns true if the value is a Const Parser */ +export function IsConst(value: unknown): value is IConst { + return IsObjectValue(value) && HasPropertyKey(value, 'type') && value.type === 'Const' && HasPropertyKey(value, 'value') && typeof value.value === 'string' +} +/** Returns true if the value is a Context Parser */ +export function IsContext(value: unknown): value is IContext { + return IsObjectValue(value) && HasPropertyKey(value, 'type') && value.type === 'Context' && HasPropertyKey(value, 'left') && IsParser(value.left) && HasPropertyKey(value, 'right') && IsParser(value.right) +} +/** Returns true if the value is a Ident Parser */ +export function IsIdent(value: unknown): value is IIdent { + return IsObjectValue(value) && HasPropertyKey(value, 'type') && value.type === 'Ident' +} +/** Returns true if the value is a Number Parser */ +export function IsNumber(value: unknown): value is INumber { + return IsObjectValue(value) && HasPropertyKey(value, 'type') && value.type === 'Number' +} +/** Returns true if the value is a Optional Parser */ +export function IsOptional(value: unknown): value is IOptional { + return IsObjectValue(value) && HasPropertyKey(value, 'type') && value.type === 'Optional' && HasPropertyKey(value, 'parser') && IsObjectValue(value.parser) +} +/** Returns true if the value is a Ref Parser */ +export function IsRef(value: unknown): value is IRef { + return IsObjectValue(value) && HasPropertyKey(value, 'type') && value.type === 'Ref' && HasPropertyKey(value, 'ref') && typeof value.ref === 'string' +} +/** Returns true if the value is a String Parser */ +export function IsString(value: unknown): value is IString { + return IsObjectValue(value) && HasPropertyKey(value, 'type') && value.type === 'String' && HasPropertyKey(value, 'options') && IsArrayValue(value.options) +} +/** Returns true if the value is a Tuple Parser */ +export function IsTuple(value: unknown): value is ITuple { + return IsObjectValue(value) && HasPropertyKey(value, 'type') && value.type === 'Tuple' && HasPropertyKey(value, 'parsers') && IsArrayValue(value.parsers) +} +/** Returns true if the value is a Union Parser */ +export function IsUnion(value: unknown): value is IUnion { + return IsObjectValue(value) && HasPropertyKey(value, 'type') && value.type === 'Union' && HasPropertyKey(value, 'parsers') && IsArrayValue(value.parsers) +} +/** Returns true if the value is a Parser */ +export function IsParser(value: unknown) { + // prettier-ignore + return ( + IsArray(value) || + IsConst(value) || + IsContext(value) || + IsIdent(value) || + IsNumber(value) || + IsOptional(value) || + IsRef(value) || + IsString(value) || + IsTuple(value) || + IsUnion(value) + ) +} diff --git a/src/parser/runtime/index.ts b/src/parser/runtime/index.ts new file mode 100644 index 000000000..3c15d4624 --- /dev/null +++ b/src/parser/runtime/index.ts @@ -0,0 +1,33 @@ +/*-------------------------------------------------------------------------- + +@sinclair/parsebox + +The MIT License (MIT) + +Copyright (c) 2024 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * as Guard from './guard' +export * as Token from './token' +export * from './types' +export * from './module' +export * from './parse' diff --git a/src/parser/runtime/module.ts b/src/parser/runtime/module.ts new file mode 100644 index 000000000..b00349b60 --- /dev/null +++ b/src/parser/runtime/module.ts @@ -0,0 +1,52 @@ +/*-------------------------------------------------------------------------- + +@sinclair/parsebox + +The MIT License (MIT) + +Copyright (c) 2024 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import * as Types from './types' +import { Parse } from './parse' + +// ------------------------------------------------------------------ +// Module +// ------------------------------------------------------------------ +export class Module { + constructor(private readonly properties: Properties) {} + + /** Parses using one of the parsers defined on this instance */ + public Parse(key: Key, content: string, context: unknown): [] | [Types.StaticParser, string] + /** Parses using one of the parsers defined on this instance */ + public Parse(key: Key, content: string): [] | [Types.StaticParser, string] + /** Parses using one of the parsers defined on this instance */ + public Parse(...args: any[]): never { + // prettier-ignore + const [key, content, context] = ( + args.length === 3 ? [args[0], args[1], args[2]] : + args.length === 2 ? [args[0], args[1], undefined] : + (() => { throw Error('Invalid parse arguments') })() + ) + return Parse(this.properties, this.properties[key], content, context) as never + } +} diff --git a/src/parser/runtime/parse.ts b/src/parser/runtime/parse.ts new file mode 100644 index 000000000..e810789cd --- /dev/null +++ b/src/parser/runtime/parse.ts @@ -0,0 +1,179 @@ +/*-------------------------------------------------------------------------- + +@sinclair/parsebox + +The MIT License (MIT) + +Copyright (c) 2024 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import * as Guard from './guard' +import * as Token from './token' +import * as Types from './types' + +// ------------------------------------------------------------------ +// Context +// ------------------------------------------------------------------ +function ParseContext(moduleProperties: ModuleProperties, left: Parser, right: Parser, code: string, context: unknown): unknown[] { + const result = ParseParser(moduleProperties, left, code, context) + return result.length === 2 ? ParseParser(moduleProperties, right, result[1], result[0]) : [] +} + +// ------------------------------------------------------------------ +// Array +// ------------------------------------------------------------------ +function ParseArray(moduleProperties: ModuleProperties, parser: Parser, code: string, context: unknown): unknown[] { + const buffer = [] as unknown[] + let rest = code + while (rest.length > 0) { + const result = ParseParser(moduleProperties, parser, rest, context) + if (result.length === 0) return [buffer, rest] + buffer.push(result[0]) + rest = result[1] + } + return [buffer, rest] +} + +// ------------------------------------------------------------------ +// Const +// ------------------------------------------------------------------ +function ParseConst(value: Value, code: string, context: unknown): [] | [Value, string] { + return Token.Const(value, code) as never +} + +// ------------------------------------------------------------------ +// Ident +// ------------------------------------------------------------------ +function ParseIdent(code: string, _context: unknown): [] | [string, string] { + return Token.Ident(code) +} + +// ------------------------------------------------------------------ +// Number +// ------------------------------------------------------------------ +// prettier-ignore +function ParseNumber(code: string, _context: unknown): [] | [string, string] { + return Token.Number(code) +} + +// ------------------------------------------------------------------ +// Optional +// ------------------------------------------------------------------ +function ParseOptional(moduleProperties: ModuleProperties, parser: Parser, code: string, context: unknown): [] | [[unknown] | [], unknown] { + const result = ParseParser(moduleProperties, parser, code, context) + return (result.length === 2 ? [[result[0]], result[1]] : [[], code]) as never +} + +// ------------------------------------------------------------------ +// Ref +// ------------------------------------------------------------------ +function ParseRef(moduleProperties: ModuleProperties, ref: Ref, code: string, context: unknown): [] | [string, string] { + const parser = moduleProperties[ref] + if (!Guard.IsParser(parser)) throw Error(`Cannot dereference Parser '${ref}'`) + return ParseParser(moduleProperties, parser, code, context) as never +} + +// ------------------------------------------------------------------ +// String +// ------------------------------------------------------------------ +// prettier-ignore +function ParseString(options: string[], code: string, _context: unknown): [] | [string, string] { + return Token.String(options, code) +} + +// ------------------------------------------------------------------ +// Tuple +// ------------------------------------------------------------------ +function ParseTuple(moduleProperties: ModuleProperties, parsers: [...Parsers], code: string, context: unknown): [] | [unknown[], string] { + const buffer = [] as unknown[] + let rest = code + for (const parser of parsers) { + const result = ParseParser(moduleProperties, parser, rest, context) + if (result.length === 0) return [] + buffer.push(result[0]) + rest = result[1] + } + return [buffer, rest] +} + +// ------------------------------------------------------------------ +// Union +// ------------------------------------------------------------------ +// prettier-ignore +function ParseUnion(moduleProperties: ModuleProperties, parsers: [...Parsers], code: string, context: unknown): [] | [unknown, string] { + for(const parser of parsers) { + const result = ParseParser(moduleProperties, parser, code, context) + if(result.length === 0) continue + return result + } + return [] +} + +// ------------------------------------------------------------------ +// Parser +// ------------------------------------------------------------------ +// prettier-ignore +function ParseParser(moduleProperties: Types.IModuleProperties, parser: Parser, code: string, context: unknown): [] | [Types.StaticParser, string] { + const result = ( + Guard.IsContext(parser) ? ParseContext(moduleProperties, parser.left, parser.right, code, context) : + Guard.IsArray(parser) ? ParseArray(moduleProperties, parser.parser, code, context) : + Guard.IsConst(parser) ? ParseConst(parser.value, code, context) : + Guard.IsIdent(parser) ? ParseIdent(code, context) : + Guard.IsNumber(parser) ? ParseNumber(code, context) : + Guard.IsOptional(parser) ? ParseOptional(moduleProperties, parser.parser, code, context) : + Guard.IsRef(parser) ? ParseRef(moduleProperties, parser.ref, code, context) : + Guard.IsString(parser) ? ParseString(parser.options, code, context) : + Guard.IsTuple(parser) ? ParseTuple(moduleProperties, parser.parsers, code, context) : + Guard.IsUnion(parser) ? ParseUnion(moduleProperties, parser.parsers, code, context) : + [] + ) + return ( + result.length === 2 + ? [parser.mapping(result[0], context), result[1]] + : result + ) as never +} + +// ------------------------------------------------------------------ +// Parse +// ------------------------------------------------------------------ +/** Parses content using the given Parser */ +// prettier-ignore +export function Parse(moduleProperties: Types.IModuleProperties, parser: Parser, code: string, context: unknown): [] | [Types.StaticParser, string] +/** Parses content using the given Parser */ +// prettier-ignore +export function Parse(moduleProperties: Types.IModuleProperties, parser: Parser, code: string): [] | [Types.StaticParser, string] +/** Parses content using the given Parser */ +// prettier-ignore +export function Parse(parser: Parser, content: string, context: unknown): [] | [Types.StaticParser, string] +/** Parses content using the given Parser */ +// prettier-ignore +export function Parse(parser: Parser, content: string): [] | [Types.StaticParser, string] +/** Parses content using the given parser */ +// prettier-ignore +export function Parse(...args: any[]): never { + const withModuleProperties = typeof args[1] === 'string' ? false : true + const [moduleProperties, parser, content, context] = withModuleProperties + ? [args[0], args[1], args[2], args[3]] + : [{}, args[0], args[1], args[2]] + return ParseParser(moduleProperties, parser, content, context) as never +} diff --git a/src/parser/runtime/token.ts b/src/parser/runtime/token.ts new file mode 100644 index 000000000..6ad9f4927 --- /dev/null +++ b/src/parser/runtime/token.ts @@ -0,0 +1,247 @@ +/*-------------------------------------------------------------------------- + +@sinclair/parsebox + +The MIT License (MIT) + +Copyright (c) 2024 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +// ------------------------------------------------------------------ +// Chars +// ------------------------------------------------------------------ +// prettier-ignore +namespace Chars { + /** Returns true if the char code is a whitespace */ + export function IsWhitespace(value: number): boolean { + return value === 32 + } + /** Returns true if the char code is a newline */ + export function IsNewline(value: number): boolean { + return value === 10 + } + /** Returns true if the char code is a alpha */ + export function IsAlpha(value: number): boolean { + return ( + (value >= 65 && value <= 90) || // A-Z + (value >= 97 && value <= 122) // a-z + ) + } + /** Returns true if the char code is zero */ + export function IsZero(value: number): boolean { + return value === 48 + } + /** Returns true if the char code is non-zero */ + export function IsNonZero(value: number): boolean { + return value >= 49 && value <= 57 + } + /** Returns true if the char code is a digit */ + export function IsDigit(value: number): boolean { + return ( + IsNonZero(value) || + IsZero(value) + ) + } + /** Returns true if the char code is a dot */ + export function IsDot(value: number): boolean { + return value === 46 + } + /** Returns true if this char code is a underscore */ + export function IsUnderscore(value: unknown): boolean { + return value === 95 + } + /** Returns true if this char code is a dollar sign */ + export function IsDollarSign(value: unknown): boolean { + return value === 36 + } +} + +// ------------------------------------------------------------------ +// Trim +// ------------------------------------------------------------------ +// prettier-ignore +namespace Trim { + /** Trims Whitespace and retains Newline, Tabspaces, etc. */ + export function TrimWhitespaceOnly(code: string): string { + for (let i = 0; i < code.length; i++) { + if (Chars.IsWhitespace(code.charCodeAt(i))) continue + return code.slice(i) + } + return code + } + /** Trims Whitespace including Newline, Tabspaces, etc. */ + export function TrimAll(code: string): string { + return code.trimStart() + } +} +// ------------------------------------------------------------------ +// Const +// ------------------------------------------------------------------ +/** Checks the value matches the next string */ +// prettier-ignore +function NextTokenCheck(value: string, code: string): boolean { + if (value.length > code.length) return false + for (let i = 0; i < value.length; i++) { + if (value.charCodeAt(i) !== code.charCodeAt(i)) return false + } + return true +} +/** Gets the next constant string value or empty if no match */ +// prettier-ignore +function NextConst(value: string, code: string, ): [] | [string, string] { + return NextTokenCheck(value, code) + ? [code.slice(0, value.length), code.slice(value.length)] + : [] +} +/** Takes the next constant string value skipping any whitespace */ +// prettier-ignore +export function Const(value: string, code: string): [] | [string, string] { + if(value.length === 0) return ['', code] + const char_0 = value.charCodeAt(0) + return ( + Chars.IsNewline(char_0) ? NextConst(value, Trim.TrimWhitespaceOnly(code)) : + Chars.IsWhitespace(char_0) ? NextConst(value, code) : + NextConst(value, Trim.TrimAll(code)) + ) +} +// ------------------------------------------------------------------ +// Ident +// ------------------------------------------------------------------ +// prettier-ignore +function IdentIsFirst(char: number) { + return ( + Chars.IsAlpha(char) || + Chars.IsDollarSign(char) || + Chars.IsUnderscore(char) + ) +} +// prettier-ignore +function IdentIsRest(char: number) { + return ( + Chars.IsAlpha(char) || + Chars.IsDigit(char) || + Chars.IsDollarSign(char) || + Chars.IsUnderscore(char) + ) +} +// prettier-ignore +function NextIdent(code: string): [] | [string, string] { + if (!IdentIsFirst(code.charCodeAt(0))) return [] + for (let i = 1; i < code.length; i++) { + const char = code.charCodeAt(i) + if (IdentIsRest(char)) continue + const slice = code.slice(0, i) + const rest = code.slice(i) + return [slice, rest] + } + return [code, ''] +} +/** Scans for the next Ident token */ +// prettier-ignore +export function Ident(code: string): [] | [string, string] { + return NextIdent(Trim.TrimAll(code)) +} +// ------------------------------------------------------------------ +// Number +// ------------------------------------------------------------------ +/** Checks that the next number is not a leading zero */ +// prettier-ignore +function NumberLeadingZeroCheck(code: string, index: number) { + const char_0 = code.charCodeAt(index + 0) + const char_1 = code.charCodeAt(index + 1) + return ( + ( + // 1-9 + Chars.IsNonZero(char_0) + ) || ( + // 0 + Chars.IsZero(char_0) && + !Chars.IsDigit(char_1) + ) || ( + // 0. + Chars.IsZero(char_0) && + Chars.IsDot(char_1) + ) || ( + // .0 + Chars.IsDot(char_0) && + Chars.IsDigit(char_1) + ) + ) +} +/** Gets the next number token */ +// prettier-ignore +function NextNumber(code: string): [] | [string, string] { + const negated = code.charAt(0) === '-' + const index = negated ? 1 : 0 + if (!NumberLeadingZeroCheck(code, index)) { + return [] + } + const dash = negated ? '-' : '' + let hasDot = false + for (let i = index; i < code.length; i++) { + const char_i = code.charCodeAt(i) + if (Chars.IsDigit(char_i)) { + continue + } + if (Chars.IsDot(char_i)) { + if (hasDot) { + const slice = code.slice(index, i) + const rest = code.slice(i) + return [`${dash}${slice}`, rest] + } + hasDot = true + continue + } + const slice = code.slice(index, i) + const rest = code.slice(i) + return [`${dash}${slice}`, rest] + } + return [code, ''] +} +/** Scans for the next number token */ +// prettier-ignore +export function Number(code: string) { + return NextNumber(Trim.TrimAll(code)) +} +// ------------------------------------------------------------------ +// String +// ------------------------------------------------------------------ +// prettier-ignore +function NextString(options: string[], code: string): [] | [string, string] { + const first = code.charAt(0) + if(!options.includes(first)) return [] + const quote = first + for(let i = 1; i < code.length; i++) { + const char = code.charAt(i) + if(char === quote) { + const slice = code.slice(1, i) + const rest = code.slice(i + 1) + return [slice, rest] + } + } + return [] +} +/** Scans the next Literal String value */ +// prettier-ignore +export function String(options: string[], code: string) { + return NextString(options, Trim.TrimAll(code)) +} diff --git a/src/parser/runtime/types.ts b/src/parser/runtime/types.ts new file mode 100644 index 000000000..c013bf5fa --- /dev/null +++ b/src/parser/runtime/types.ts @@ -0,0 +1,250 @@ +/*-------------------------------------------------------------------------- + +@sinclair/parsebox + +The MIT License (MIT) + +Copyright (c) 2024 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export type IModuleProperties = Record + +// ------------------------------------------------------------------ +// Static +// ------------------------------------------------------------------ + +/** Force output static type evaluation for Arrays */ +export type StaticEnsure = T extends infer R ? R : never + +/** Infers the Output Parameter for a Parser */ +export type StaticParser = Parser extends IParser ? Output : unknown + +// ------------------------------------------------------------------ +// Mapping +// ------------------------------------------------------------------ +export type IMapping = (input: Input, context: any) => Output + +/** Maps input to output. This is the default Mapping */ +export const Identity = (value: unknown) => value + +/** Maps the output as the given parameter T */ +// prettier-ignore +export const As = (mapping: T): ((value: unknown) => T) => (_: unknown) => mapping + +// ------------------------------------------------------------------ +// Parser +// ------------------------------------------------------------------ +export interface IParser { + type: string + mapping: IMapping +} +// ------------------------------------------------------------------ +// Context +// ------------------------------------------------------------------ +// prettier-ignore +export type ContextParameter<_Left extends IParser, Right extends IParser> = ( + StaticParser +) +export interface IContext extends IParser { + type: 'Context' + left: IParser + right: IParser +} +/** `[Context]` Creates a Context Parser */ +export function Context>>(left: Left, right: Right, mapping: Mapping): IContext> +/** `[Context]` Creates a Context Parser */ +export function Context(left: Left, right: Right): IContext> +/** `[Context]` Creates a Context Parser */ +export function Context(...args: unknown[]): never { + const [left, right, mapping] = args.length === 3 ? [args[0], args[1], args[2]] : [args[0], args[1], Identity] + return { type: 'Context', left, right, mapping } as never +} +// ------------------------------------------------------------------ +// Array +// ------------------------------------------------------------------ +// prettier-ignore +export type ArrayParameter = StaticEnsure< + StaticParser[] +> +export interface IArray extends IParser { + type: 'Array' + parser: IParser +} +/** `[EBNF]` Creates an Array Parser */ +export function Array>>(parser: Parser, mapping: Mapping): IArray> +/** `[EBNF]` Creates an Array Parser */ +export function Array(parser: Parser): IArray> +/** `[EBNF]` Creates an Array Parser */ +export function Array(...args: unknown[]): never { + const [parser, mapping] = args.length === 2 ? [args[0], args[1]] : [args[0], Identity] + return { type: 'Array', parser, mapping } as never +} + +// ------------------------------------------------------------------ +// Const +// ------------------------------------------------------------------ +export interface IConst extends IParser { + type: 'Const' + value: string +} +/** `[TERM]` Creates a Const Parser */ +export function Const>(value: Value, mapping: Mapping): IConst> +/** `[TERM]` Creates a Const Parser */ +export function Const(value: Value): IConst +/** `[TERM]` Creates a Const Parser */ +export function Const(...args: unknown[]): never { + const [value, mapping] = args.length === 2 ? [args[0], args[1]] : [args[0], Identity] + return { type: 'Const', value, mapping } as never +} + +// ------------------------------------------------------------------ +// Ref +// ------------------------------------------------------------------ +export interface IRef extends IParser { + type: 'Ref' + ref: string +} +/** `[BNF]` Creates a Ref Parser. This Parser can only be used in the context of a Module */ +export function Ref>(ref: string, mapping: Mapping): IRef> +/** `[BNF]` Creates a Ref Parser. This Parser can only be used in the context of a Module */ +export function Ref(ref: string): IRef +/** `[BNF]` Creates a Ref Parser. This Parser can only be used in the context of a Module */ +export function Ref(...args: unknown[]): never { + const [ref, mapping] = args.length === 2 ? [args[0], args[1]] : [args[0], Identity] + return { type: 'Ref', ref, mapping } as never +} + +// ------------------------------------------------------------------ +// String +// ------------------------------------------------------------------ +export interface IString extends IParser { + type: 'String' + options: string[] +} +/** `[TERM]` Creates a String Parser. Options are an array of permissable quote characters */ +export function String>(options: string[], mapping: Mapping): IString> +/** `[TERM]` Creates a String Parser. Options are an array of permissable quote characters */ +export function String(options: string[]): IString +/** `[TERM]` Creates a String Parser. Options are an array of permissable quote characters */ +export function String(...params: unknown[]): never { + const [options, mapping] = params.length === 2 ? [params[0], params[1]] : [params[0], Identity] + return { type: 'String', options, mapping } as never +} + +// ------------------------------------------------------------------ +// Ident +// ------------------------------------------------------------------ +export interface IIdent extends IParser { + type: 'Ident' +} +/** `[TERM]` Creates an Ident Parser where Ident matches any valid JavaScript identifier */ +export function Ident>(mapping: Mapping): IIdent> +/** `[TERM]` Creates an Ident Parser where Ident matches any valid JavaScript identifier */ +export function Ident(): IIdent +/** `[TERM]` Creates an Ident Parser where Ident matches any valid JavaScript identifier */ +export function Ident(...params: unknown[]): never { + const mapping = params.length === 1 ? params[0] : Identity + return { type: 'Ident', mapping } as never +} + +// ------------------------------------------------------------------ +// Number +// ------------------------------------------------------------------ +export interface INumber extends IParser { + type: 'Number' +} +/** `[TERM]` Creates an Number Parser */ +export function Number>(mapping: Mapping): INumber> +/** `[TERM]` Creates an Number Parser */ +export function Number(): INumber +/** `[TERM]` Creates an Number Parser */ +export function Number(...params: unknown[]): never { + const mapping = params.length === 1 ? params[0] : Identity + return { type: 'Number', mapping } as never +} + +// ------------------------------------------------------------------ +// Optional +// ------------------------------------------------------------------ +// prettier-ignore +export type OptionalParameter] | []> = ( + Result +) +export interface IOptional extends IParser { + type: 'Optional' + parser: IParser +} +/** `[EBNF]` Creates an Optional Parser */ +export function Optional>>(parser: Parser, mapping: Mapping): IOptional> +/** `[EBNF]` Creates an Optional Parser */ +export function Optional(parser: Parser): IOptional> +/** `[EBNF]` Creates an Optional Parser */ +export function Optional(...args: unknown[]): never { + const [parser, mapping] = args.length === 2 ? [args[0], args[1]] : [args[0], Identity] + return { type: 'Optional', parser, mapping } as never +} + +// ------------------------------------------------------------------ +// Tuple +// ------------------------------------------------------------------ +// prettier-ignore +export type TupleParameter = StaticEnsure< + Parsers extends [infer Left extends IParser, ...infer Right extends IParser[]] + ? TupleParameter>]> + : Result +> +export interface ITuple extends IParser { + type: 'Tuple' + parsers: IParser[] +} +/** `[BNF]` Creates a Tuple Parser */ +export function Tuple>>(parsers: [...Parsers], mapping: Mapping): ITuple> +/** `[BNF]` Creates a Tuple Parser */ +export function Tuple(parsers: [...Parsers]): ITuple> +/** `[BNF]` Creates a Tuple Parser */ +export function Tuple(...args: unknown[]): never { + const [parsers, mapping] = args.length === 2 ? [args[0], args[1]] : [args[0], Identity] + return { type: 'Tuple', parsers, mapping } as never +} + +// ------------------------------------------------------------------ +// Union +// ------------------------------------------------------------------ +// prettier-ignore +export type UnionParameter = StaticEnsure< + Parsers extends [infer Left extends IParser, ...infer Right extends IParser[]] + ? UnionParameter> + : Result +> +export interface IUnion extends IParser { + type: 'Union' + parsers: IParser[] +} +/** `[BNF]` Creates a Union parser */ +export function Union>>(parsers: [...Parsers], mapping: Mapping): IUnion> +/** `[BNF]` Creates a Union parser */ +export function Union(parsers: [...Parsers]): IUnion> +/** `[BNF]` Creates a Union parser */ +export function Union(...args: unknown[]): never { + const [parsers, mapping] = args.length === 2 ? [args[0], args[1]] : [args[0], Identity] + return { type: 'Union', parsers, mapping } as never +} diff --git a/src/parser/static/index.ts b/src/parser/static/index.ts new file mode 100644 index 000000000..3eb58ee18 --- /dev/null +++ b/src/parser/static/index.ts @@ -0,0 +1,31 @@ +/*-------------------------------------------------------------------------- + +@sinclair/parsebox + +The MIT License (MIT) + +Copyright (c) 2024 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * as Token from './token' +export * from './parse' +export * from './types' diff --git a/src/parser/static/parse.ts b/src/parser/static/parse.ts new file mode 100644 index 000000000..f19eaad9c --- /dev/null +++ b/src/parser/static/parse.ts @@ -0,0 +1,153 @@ +/*-------------------------------------------------------------------------- + +@sinclair/parsebox + +The MIT License (MIT) + +Copyright (c) 2024 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import * as Tokens from './token' +import * as Types from './types' + +// ------------------------------------------------------------------ +// Context +// ------------------------------------------------------------------ +// prettier-ignore +type ContextParser = ( + Parse extends [infer Context extends unknown, infer Rest extends string] + ? Parse + : [] +) + +// ------------------------------------------------------------------ +// Array +// ------------------------------------------------------------------ +// prettier-ignore +type ArrayParser = ( + Parse extends [infer Value1 extends unknown, infer Rest extends string] + ? ArrayParser + : [Result, Code] +) + +// ------------------------------------------------------------------ +// Const +// ------------------------------------------------------------------ +// prettier-ignore +type ConstParser = ( + Tokens.Const extends [infer Match extends Value, infer Rest extends string] + ? [Match, Rest] + : [] +) + +// ------------------------------------------------------------------ +// Ident +// ------------------------------------------------------------------ +// prettier-ignore +type IdentParser = ( + Tokens.Ident extends [infer Match extends string, infer Rest extends string] + ? [Match, Rest] + : [] +) + +// ------------------------------------------------------------------ +// Number +// ------------------------------------------------------------------ +// prettier-ignore +type NumberParser = ( + Tokens.Number extends [infer Match extends string, infer Rest extends string] + ? [Match, Rest] + : [] +) + +// ------------------------------------------------------------------ +// Optional +// ------------------------------------------------------------------ +// prettier-ignore +type OptionalParser = ( + Parse extends [infer Value extends unknown, infer Rest extends string] + ? [[Value], Rest] + : [[], Code] +) + +// ------------------------------------------------------------------ +// String +// ------------------------------------------------------------------ +// prettier-ignore +type StringParser = ( + Tokens.String extends [infer Match extends string, infer Rest extends string] + ? [Match, Rest] + : [] +) + +// ------------------------------------------------------------------ +// Tuple +// ------------------------------------------------------------------ +// prettier-ignore +type TupleParser = ( + Parsers extends [infer Left extends Types.IParser, ...infer Right extends Types.IParser[]] + ? Parse extends [infer Value extends unknown, infer Rest extends string] + ? TupleParser + : [] + : [Result, Code] +) + +// ------------------------------------------------------------------ +// Union +// ------------------------------------------------------------------ +// prettier-ignore +type UnionParser = ( + Parsers extends [infer Left extends Types.IParser, ...infer Right extends Types.IParser[]] + ? Parse extends [infer Value extends unknown, infer Rest extends string] + ? [Value, Rest] + : UnionParser + : [] +) + +// ------------------------------------------------------------------ +// Parse +// ------------------------------------------------------------------ +// prettier-ignore +type ParseCode = ( + Type extends Types.Context ? ContextParser : + Type extends Types.Array ? ArrayParser : + Type extends Types.Const ? ConstParser : + Type extends Types.Ident ? IdentParser : + Type extends Types.Number ? NumberParser : + Type extends Types.Optional ? OptionalParser : + Type extends Types.String ? StringParser : + Type extends Types.Tuple ? TupleParser : + Type extends Types.Union ? UnionParser : + [] +) +// prettier-ignore +type ParseMapping = ( + (Parser['mapping'] & { input: Result, context: Context })['output'] +) + +/** Parses code with the given parser */ +// prettier-ignore +export type Parse = ( + ParseCode extends [infer L extends unknown, infer R extends string] + ? [ParseMapping, R] + : [] +) diff --git a/src/parser/static/token.ts b/src/parser/static/token.ts new file mode 100644 index 000000000..8bfdf19dd --- /dev/null +++ b/src/parser/static/token.ts @@ -0,0 +1,213 @@ +/*-------------------------------------------------------------------------- + +@sinclair/parsebox + +The MIT License (MIT) + +Copyright (c) 2024 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +// ------------------------------------------------------------------ +// Chars +// ------------------------------------------------------------------ +// prettier-ignore +namespace Chars { + export type Empty = '' + export type Space = ' ' + export type Newline = '\n' + export type Dot = '.' + export type Hyphen = '-' + export type Digit = [ + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' + ] + export type Alpha = [ + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', + 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', + 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', + 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', + 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', + 'Y', 'Z' + ] +} + +// ------------------------------------------------------------------ +// Trim +// ------------------------------------------------------------------ +// prettier-ignore +namespace Trim { + // ------------------------------------------------------------------ + // Whitespace Filters + // ------------------------------------------------------------------ + type W9 = `${W8}${W8}` // 512 + type W8 = `${W7}${W7}` // 256 + type W7 = `${W6}${W6}` // 128 + type W6 = `${W5}${W5}` // 64 + type W5 = `${W4}${W4}` // 32 + type W4 = `${W3}${W3}` // 16 + type W3 = `${W2}${W2}` // 8 + type W2 = `${W1}${W1}` // 4 + type W1 = `${W0}${W0}` // 2 + type W0 = ` ` // 1 + // ------------------------------------------------------------------ + // TrimWhitespace + // ------------------------------------------------------------------ + /** Trims whitespace only */ + export type TrimWhitespace = ( + Code extends `${W4}${infer Rest extends string}` ? TrimWhitespace : + Code extends `${W3}${infer Rest extends string}` ? TrimWhitespace : + Code extends `${W1}${infer Rest extends string}` ? TrimWhitespace : + Code extends `${W0}${infer Rest extends string}` ? TrimWhitespace : + Code + ) + // ------------------------------------------------------------------ + // Trim + // ------------------------------------------------------------------ + /** Trims Whitespace and Newline */ + export type TrimAll = ( + Code extends `${W4}${infer Rest extends string}` ? TrimAll : + Code extends `${W3}${infer Rest extends string}` ? TrimAll : + Code extends `${W1}${infer Rest extends string}` ? TrimAll : + Code extends `${W0}${infer Rest extends string}` ? TrimAll : + Code extends `${Chars.Newline}${infer Rest extends string}` ? TrimAll : + Code + ) +} + +// ------------------------------------------------------------------ +// Union +// ------------------------------------------------------------------ +/** Scans for the next match union */ +// prettier-ignore +type NextUnion = ( + Variants extends [infer Variant extends string, ...infer Rest1 extends string[]] + ? NextConst extends [infer Match extends string, infer Rest2 extends string] + ? [Match, Rest2] + : NextUnion + : [] +) + +// ------------------------------------------------------------------ +// Const +// ------------------------------------------------------------------ +// prettier-ignore +type NextConst = ( + Code extends `${Value}${infer Rest extends string}` + ? [Value, Rest] + : [] +) +/** Scans for the next constant value */ +// prettier-ignore +export type Const = ( + Value extends '' ? ['', Code] : + Value extends `${infer First extends string}${string}` + ? ( + First extends Chars.Newline ? NextConst> : + First extends Chars.Space ? NextConst : + NextConst> + ) : never +) +// ------------------------------------------------------------------ +// Number +// ------------------------------------------------------------------ +// prettier-ignore +type NextNumberNegate = ( + Code extends `${Chars.Hyphen}${infer Rest extends string}` + ? [Chars.Hyphen, Rest] + : [Chars.Empty, Code] +) +// prettier-ignore +type NextNumberZeroCheck = ( + Code extends `0${infer Rest}` + ? NextUnion extends [string, string] ? false : true + : true +) +// prettier-ignore +type NextNumberScan = ( + NextUnion<[...Chars.Digit, Chars.Dot], Code> extends [infer Char extends string, infer Rest extends string] + ? Char extends Chars.Dot + ? HasDecimal extends false + ? NextNumberScan + : [Result, `.${Rest}`] + : NextNumberScan + : [Result, Code] +) +// prettier-ignore +export type NextNumber = ( + NextNumberNegate extends [infer Negate extends string, infer Rest extends string] + ? NextNumberZeroCheck extends true + ? NextNumberScan extends [infer Number extends string, infer Rest2 extends string] + ? Number extends Chars.Empty + ? [] + : [`${Negate}${Number}`, Rest2] + : [] + : [] + : [] +) +/** Scans for the next literal number */ +export type Number = NextNumber> + +// ------------------------------------------------------------------ +// String +// ------------------------------------------------------------------ +type NextStringQuote = NextUnion +// prettier-ignore +type NextStringBody = ( + Code extends `${infer Char extends string}${infer Rest extends string}` + ? Char extends Quote + ? [Result, Rest] + : NextStringBody + : [] +) +// prettier-ignore +type NextString = ( + NextStringQuote extends [infer Quote extends string, infer Rest extends string] + ? NextStringBody extends [infer String extends string, infer Rest extends string] + ? [String, Rest] + : [] + : [] +) +/** Scans for the next literal string */ +export type String = NextString> + +// ------------------------------------------------------------------ +// Ident +// ------------------------------------------------------------------ +type IdentLeft = [...Chars.Alpha, '_', '$'] // permissable first characters +type IdentRight = [...Chars.Digit, ...IdentLeft] // permissible subsequent characters + +// prettier-ignore +type NextIdentScan = ( + NextUnion extends [infer Char extends string, infer Rest extends string] + ? NextIdentScan + : [Result, Code] +) +// prettier-ignore +type NextIdent = ( + NextUnion extends [infer Left extends string, infer Rest1 extends string] + ? NextIdentScan extends [infer Right extends string, infer Rest2 extends string] + ? [`${Left}${Right}`, Rest2] + : [] + : [] +) + +/** Scans for the next Ident */ +export type Ident = NextIdent> diff --git a/src/parser/static/types.ts b/src/parser/static/types.ts new file mode 100644 index 000000000..5adbc306c --- /dev/null +++ b/src/parser/static/types.ts @@ -0,0 +1,143 @@ +/*-------------------------------------------------------------------------- + +@sinclair/parsebox + +The MIT License (MIT) + +Copyright (c) 2024 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +// ------------------------------------------------------------------ +// Mapping +// ------------------------------------------------------------------ +/** + * `[ACTION]` Inference mapping base type. Used to specify semantic actions for + * Parser productions. This type is implemented as a higher-kinded type where + * productions are received on the `input` property with mapping assigned + * the `output` property. The parsing context is available on the `context` + * property. + */ +export interface IMapping { + context: unknown + input: unknown + output: unknown +} + +/** `[ACTION]` Default inference mapping. */ +export interface Identity extends IMapping { + output: this['input'] +} + +/** `[ACTION]` Maps the given argument `T` as the mapping output */ +export interface As extends IMapping { + output: T +} +// ------------------------------------------------------------------ +// Parser +// ------------------------------------------------------------------ +/** Base type Parser implemented by all other parsers */ +export interface IParser { + type: string + mapping: Mapping +} + +// ------------------------------------------------------------------ +// Context +// ------------------------------------------------------------------ +/** `[Context]` Creates a Context Parser */ +export interface Context extends IParser { + type: 'Context' + left: Left + right: Right +} + +// ------------------------------------------------------------------ +// Array +// ------------------------------------------------------------------ +/** `[EBNF]` Creates an Array Parser */ +export interface Array extends IParser { + type: 'Array' + parser: Parser +} + +// ------------------------------------------------------------------ +// Const +// ------------------------------------------------------------------ +/** `[TERM]` Creates a Const Parser */ +export interface Const extends IParser { + type: 'Const' + value: Value +} + +// ------------------------------------------------------------------ +// Ident +// ------------------------------------------------------------------ +/** `[TERM]` Creates an Ident Parser. */ +// prettier-ignore +export interface Ident extends IParser { + type: 'Ident' +} + +// ------------------------------------------------------------------ +// Number +// ------------------------------------------------------------------ +/** `[TERM]` Creates a Number Parser. */ +// prettier-ignore +export interface Number extends IParser { + type: 'Number' +} + +// ------------------------------------------------------------------ +// Optional +// ------------------------------------------------------------------ +/** `[EBNF]` Creates a Optional Parser */ +export interface Optional extends IParser { + type: 'Optional' + parser: Parser +} + +// ------------------------------------------------------------------ +// String +// ------------------------------------------------------------------ +/** `[TERM]` Creates a String Parser. Options are an array of permissable quote characters */ +export interface String extends IParser { + type: 'String' + quote: Options +} + +// ------------------------------------------------------------------ +// Tuple +// ------------------------------------------------------------------ +/** `[BNF]` Creates a Tuple Parser */ +export interface Tuple extends IParser { + type: 'Tuple' + parsers: [...Parsers] +} + +// ------------------------------------------------------------------ +// Union +// ------------------------------------------------------------------ +/** `[BNF]` Creates a Union Parser */ +export interface Union extends IParser { + type: 'Union' + parsers: [...Parsers] +} diff --git a/src/syntax/index.ts b/src/syntax/index.ts new file mode 100644 index 000000000..d7fc819f7 --- /dev/null +++ b/src/syntax/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/syntax + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './syntax' diff --git a/src/syntax/mapping.ts b/src/syntax/mapping.ts new file mode 100644 index 000000000..60061759d --- /dev/null +++ b/src/syntax/mapping.ts @@ -0,0 +1,1030 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/syntax + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import * as T from '../type/index' + +// ------------------------------------------------------------------ +// +// Dereference +// +// Referential types pull from the Context or defer dereferencing +// for later execution. This overlaps with module dereferencing, +// where named identifiers in the syntax are deferred until +// instantiation. This code should be revised as part of a +// general-purpose Instantiate module (next revision) +// +// ------------------------------------------------------------------ +// prettier-ignore +type TDereference = ( + Key extends keyof Context ? Context[Key] : T.TRef +) +// prettier-ignore +const Dereference = (context: T.TProperties, key: string): T.TSchema => { + return key in context ? context[key] : T.Ref(key) +} + +// ------------------------------------------------------------------ +// +// Delimited +// +// Delimited sequences use an accumulated buffer to parse sequence +// tokens. This approach is more scalable than using a Union + Tuple +// + Epsilon pattern, as TypeScript can instantiate deeper when +// tail-call recursive accumulators are employed. However, this +// comes with a latent processing cost due to the need to decode +// the accumulated buffer. +// +// - Encoding: [[, ','][], [] | []] +// +// ------------------------------------------------------------------ +// prettier-ignore +type TDelimitedDecode = ( + Input extends [infer Left, ...infer Right] + ? Left extends [infer Item, infer _] + ? TDelimitedDecode + : TDelimitedDecode + : Result +) +// prettier-ignore +type TDelimited + = Input extends [infer Left extends unknown[], infer Right extends unknown[]] + ? TDelimitedDecode<[...Left, ...Right]> + : [] +// prettier-ignore +const DelimitedDecode = (input: ([unknown, unknown] | unknown)[], result: unknown[] = []) => { + return input.reduce((result, left) => { + return T.ValueGuard.IsArray(left) && left.length === 2 + ? [...result, left[0]] + : [...result, left] + }, []) +} +// prettier-ignore +const Delimited = (input: [unknown, unknown]) => { + const [left, right] = input as [unknown[], unknown[]] + return DelimitedDecode([...left, ...right]) +} +// ------------------------------------------------------------------- +// GenericReferenceParameterList: [[Type, ','][], [Type] | []] +// ------------------------------------------------------------------- +// prettier-ignore +export type TGenericReferenceParameterListMapping + = TDelimited +// prettier-ignore +export function GenericReferenceParameterListMapping(input: [unknown, unknown], context: unknown) { + return Delimited(input) +} +// ------------------------------------------------------------------- +// GenericReference: [, '<', GenericReferenceParameterList, '>'] +// ------------------------------------------------------------------- +// prettier-ignore +export type TGenericReferenceMapping'] + ? T.TInstantiate, Args> + : never + : never +> = Result +// prettier-ignore +export function GenericReferenceMapping(input: [unknown, unknown, unknown, unknown], context: unknown) { + const type = Dereference(context as T.TProperties, input[0] as string) + const args = input[2] as T.TSchema[] + return T.Instantiate(type, args) +} +// ------------------------------------------------------------------- +// GenericArgumentsList: [[, ','][], [] | []] +// ------------------------------------------------------------------- +// prettier-ignore +export type TGenericArgumentsListMapping + = TDelimited +// prettier-ignore +export function GenericArgumentsListMapping(input: [unknown, unknown], context: unknown) { + return Delimited(input) +} +// ------------------------------------------------------------------- +// GenericArguments: ['<', GenericArgumentsList, '>'] +// ------------------------------------------------------------------- +// prettier-ignore +type GenericArgumentsContext = ( + Arguments extends [...infer Left extends string[], infer Right extends string] + ? GenericArgumentsContext }> + : T.Evaluate +) +// prettier-ignore +export type TGenericArgumentsMapping = + Input extends ['<', infer Arguments extends string[], '>'] + ? Context extends infer Context extends T.TProperties + ? GenericArgumentsContext + : never + : never +// ... +// prettier-ignore +const GenericArgumentsContext = (_arguments: string[], context: T.TProperties) => { + return _arguments.reduce((result, arg, index) => { + return { ...result, [arg]: T.Argument(index) } + }, context) +} +// prettier-ignore +export function GenericArgumentsMapping(input: [unknown, unknown, unknown], context: unknown) { + return input.length === 3 + ? GenericArgumentsContext(input[1] as string[], context as T.TProperties) + : {} +} +// ------------------------------------------------------------------- +// KeywordString: 'string' +// ------------------------------------------------------------------- +// prettier-ignore +export type TKeywordStringMapping + = T.TString +// prettier-ignore +export function KeywordStringMapping(input: 'string', context: unknown) { + return T.String() +} +// ------------------------------------------------------------------- +// KeywordNumber: 'number' +// ------------------------------------------------------------------- +// prettier-ignore +export type TKeywordNumberMapping + = T.TNumber +// prettier-ignore +export function KeywordNumberMapping(input: 'number', context: unknown) { + return T.Number() +} +// ------------------------------------------------------------------- +// KeywordBoolean: 'boolean' +// ------------------------------------------------------------------- +// prettier-ignore +export type TKeywordBooleanMapping + = T.TBoolean +// prettier-ignore +export function KeywordBooleanMapping(input: 'boolean', context: unknown) { + return T.Boolean() +} +// ------------------------------------------------------------------- +// KeywordUndefined: 'undefined' +// ------------------------------------------------------------------- +// prettier-ignore +export type TKeywordUndefinedMapping + = T.TUndefined +// prettier-ignore +export function KeywordUndefinedMapping(input: 'undefined', context: unknown) { + return T.Undefined() +} +// ------------------------------------------------------------------- +// KeywordNull: 'null' +// ------------------------------------------------------------------- +// prettier-ignore +export type TKeywordNullMapping + = T.TNull +// prettier-ignore +export function KeywordNullMapping(input: 'null', context: unknown) { + return T.Null() +} +// ------------------------------------------------------------------- +// KeywordInteger: 'integer' +// ------------------------------------------------------------------- +// prettier-ignore +export type TKeywordIntegerMapping + = T.TInteger +// prettier-ignore +export function KeywordIntegerMapping(input: 'integer', context: unknown) { + return T.Integer() +} +// ------------------------------------------------------------------- +// KeywordBigInt: 'bigint' +// ------------------------------------------------------------------- +// prettier-ignore +export type TKeywordBigIntMapping + = T.TBigInt +// prettier-ignore +export function KeywordBigIntMapping(input: 'bigint', context: unknown) { + return T.BigInt() +} +// ------------------------------------------------------------------- +// KeywordUnknown: 'unknown' +// ------------------------------------------------------------------- +// prettier-ignore +export type TKeywordUnknownMapping + = T.TUnknown +// prettier-ignore +export function KeywordUnknownMapping(input: 'unknown', context: unknown) { + return T.Unknown() +} +// ------------------------------------------------------------------- +// KeywordAny: 'any' +// ------------------------------------------------------------------- +// prettier-ignore +export type TKeywordAnyMapping + = T.TAny +// prettier-ignore +export function KeywordAnyMapping(input: 'any', context: unknown) { + return T.Any() +} +// ------------------------------------------------------------------- +// KeywordNever: 'never' +// ------------------------------------------------------------------- +// prettier-ignore +export type TKeywordNeverMapping + = T.TNever +// prettier-ignore +export function KeywordNeverMapping(input: 'never', context: unknown) { + return T.Never() +} +// ------------------------------------------------------------------- +// KeywordSymbol: 'symbol' +// ------------------------------------------------------------------- +// prettier-ignore +export type TKeywordSymbolMapping + = T.TSymbol +// prettier-ignore +export function KeywordSymbolMapping(input: 'symbol', context: unknown) { + return T.Symbol() +} +// ------------------------------------------------------------------- +// KeywordVoid: 'void' +// ------------------------------------------------------------------- +// prettier-ignore +export type TKeywordVoidMapping + = T.TVoid +// prettier-ignore +export function KeywordVoidMapping(input: 'void', context: unknown) { + return T.Void() +} +// ------------------------------------------------------------------- +// Keyword: KeywordString | KeywordNumber | KeywordBoolean | KeywordUndefined | KeywordNull | KeywordInteger | KeywordBigInt | KeywordUnknown | KeywordAny | KeywordNever | KeywordSymbol | KeywordVoid +// ------------------------------------------------------------------- +// prettier-ignore +export type TKeywordMapping + = Input +// prettier-ignore +export function KeywordMapping(input: unknown, context: unknown) { + return input +} +// ------------------------------------------------------------------- +// LiteralString: +// ------------------------------------------------------------------- +// prettier-ignore +export type TLiteralStringMapping = + Input extends T.TLiteralValue ? T.TLiteral : never +// prettier-ignore +export function LiteralStringMapping(input: string, context: unknown) { + return T.Literal(input) +} +// ------------------------------------------------------------------- +// LiteralNumber: +// ------------------------------------------------------------------- +// prettier-ignore +export type TLiteralNumberMapping = + Input extends `${infer Value extends number}` ? T.TLiteral : never +// prettier-ignore +export function LiteralNumberMapping(input: string, context: unknown) { + return T.Literal(parseFloat(input)) +} +// ------------------------------------------------------------------- +// LiteralBoolean: 'true' | 'false' +// ------------------------------------------------------------------- +// prettier-ignore +export type TLiteralBooleanMapping + = Input extends 'true' ? T.TLiteral : T.TLiteral +// prettier-ignore +export function LiteralBooleanMapping(input: 'true' | 'false', context: unknown) { + return T.Literal(input === 'true') +} +// ------------------------------------------------------------------- +// Literal: LiteralBoolean | LiteralNumber | LiteralString +// ------------------------------------------------------------------- +// prettier-ignore +export type TLiteralMapping + = Input +// prettier-ignore +export function LiteralMapping(input: unknown, context: unknown) { + return input +} +// ------------------------------------------------------------------- +// KeyOf: ['keyof'] | [] +// ------------------------------------------------------------------- +// prettier-ignore +export type TKeyOfMapping + = Input extends [unknown] ? true : false +// prettier-ignore +export function KeyOfMapping(input: [unknown] | [], context: unknown) { + return input.length > 0 +} +// ------------------------------------------------------------------- +// IndexArray: ['[', Type, ']'] | ['[', ']'][] +// ------------------------------------------------------------------- +// prettier-ignore +type TIndexArrayMappingReduce = ( + Input extends [infer Left extends unknown, ...infer Right extends unknown[]] + ? Left extends ['[', infer Type extends T.TSchema, ']'] + ? TIndexArrayMappingReduce + : TIndexArrayMappingReduce + : Result +) +// prettier-ignore +export type TIndexArrayMapping + = Input extends unknown[] + ? TIndexArrayMappingReduce + : [] +// prettier-ignore +export function IndexArrayMapping(input: ([unknown, unknown, unknown] | [unknown, unknown])[], context: unknown) { + return input.reduce((result: unknown[], current) => { + return current.length === 3 + ? [...result, [current[1]]] + : [...result, []] + }, [] as unknown[]) +} +// ------------------------------------------------------------------- +// Extends: ['extends', Type, '?', Type, ':', Type] | [] +// ------------------------------------------------------------------- +// prettier-ignore +export type TExtendsMapping + = Input extends ['extends', infer Type extends T.TSchema, '?', infer True extends T.TSchema, ':', infer False extends T.TSchema] + ? [Type, True, False] + : [] +// prettier-ignore +export function ExtendsMapping(input: [unknown, unknown, unknown, unknown, unknown, unknown] | [], context: unknown) { + return input.length === 6 + ? [input[1], input[3], input[5]] + : [] +} +// ------------------------------------------------------------------- +// Base: ['(', Type, ')'] | Keyword | Object | Tuple | Literal | Constructor | Function | Mapped | AsyncIterator | Iterator | ConstructorParameters | FunctionParameters | InstanceType | ReturnType | Argument | Awaited | Array | Record | Promise | Partial | Required | Pick | Omit | Exclude | Extract | Uppercase | Lowercase | Capitalize | Uncapitalize | Date | Uint8Array | GenericReference | Reference +// ------------------------------------------------------------------- +// prettier-ignore +export type TBaseMapping = ( + Input extends ['(', infer Type extends T.TSchema, ')'] ? Type : + Input extends infer Type extends T.TSchema ? Type : + never +) +// prettier-ignore +export function BaseMapping(input: [unknown, unknown, unknown] | unknown, context: unknown) { + return T.ValueGuard.IsArray(input) && input.length === 3 ? input[1] : input +} +// ------------------------------------------------------------------- +// Factor: [KeyOf, Base, IndexArray, Extends] +// ------------------------------------------------------------------- +// prettier-ignore +type TFactorIndexArray = ( + IndexArray extends [...infer Left extends unknown[], infer Right extends T.TSchema[]] ? ( + Right extends [infer Indexer extends T.TSchema] ? T.TIndex, T.TIndexPropertyKeys> : + Right extends [] ? T.TArray> : + T.TNever + ) : Type +) +// prettier-ignore +type TFactorExtends = ( + Extends extends [infer Right extends T.TSchema, infer True extends T.TSchema, infer False extends T.TSchema] + ? T.TExtends + : Type +) +// prettier-ignore +export type TFactorMapping = + Input extends [infer KeyOf extends boolean, infer Type extends T.TSchema, infer IndexArray extends unknown[], infer Extends extends unknown[]] + ? KeyOf extends true + ? TFactorExtends>, Extends> + : TFactorExtends, Extends> + : never + +// ... +// prettier-ignore +const FactorIndexArray = (Type: T.TSchema, indexArray: unknown[]): T.TSchema => { + return indexArray.reduceRight((result, right) => { + const _right = right as T.TSchema[] + return ( + _right.length === 1 ? T.Index(result, _right[0]) : + _right.length === 0 ? T.Array(result, _right[0]) : + T.Never() + ) + }, Type) +} +// prettier-ignore +const FactorExtends = (Type: T.TSchema, Extends: T.TSchema[]) => { + return Extends.length === 3 + ? T.Extends(Type, Extends[0], Extends[1], Extends[2]) + : Type +} +// prettier-ignore +export function FactorMapping(input: [unknown, unknown, unknown, unknown], context: unknown) { + const [KeyOf, Type, IndexArray, Extends] = input as [boolean, T.TSchema, unknown[], T.TSchema[]] + return KeyOf + ? FactorExtends(T.KeyOf(FactorIndexArray(Type, IndexArray)), Extends) + : FactorExtends(FactorIndexArray(Type, IndexArray), Extends) +} + +// ------------------------------------------------------------------ +// +// ExprBinaryMapping +// +// TypeBox Union and Intersection types are flattened to prevent +// excessive nesting of `anyOf` and `allOf`, ensuring a more +// readable and presentable type for the user. This function +// recursively reduces Union and Intersection types based on +// binary expressions parsed from input. +// +// ------------------------------------------------------------------ +// prettier-ignore +type TExprBinaryMapping = ( + Rest extends [infer Operator extends unknown, infer Right extends T.TSchema, infer Next extends unknown[]] ? ( + TExprBinaryMapping extends infer Schema extends T.TSchema ? ( + Operator extends '&' ? ( + Schema extends T.TIntersect + ? T.TIntersect<[Left, ...Types]> + : T.TIntersect<[Left, Schema]> + ) : + Operator extends '|' ? ( + Schema extends T.TUnion + ? T.TUnion<[Left, ...Types]> + : T.TUnion<[Left, Schema]> + ) : never + ) : never + ) : Left +) +// prettier-ignore +function ExprBinaryMapping(Left: T.TSchema, Rest: unknown[]): T.TSchema { + return ( + Rest.length === 3 ? (() => { + const [Operator, Right, Next] = Rest as [string, T.TSchema, unknown[]] + const Schema = ExprBinaryMapping(Right, Next) + if (Operator === '&') { + return T.TypeGuard.IsIntersect(Schema) + ? T.Intersect([Left, ...Schema.allOf]) + : T.Intersect([Left, Schema]) + } + if (Operator === '|') { + return T.TypeGuard.IsUnion(Schema) + ? T.Union([Left, ...Schema.anyOf]) + : T.Union([Left, Schema]) + } + throw 1 + })() : Left + ) +} +// ------------------------------------------------------------------- +// ExprTermTail: ['&', Factor, ExprTermTail] | [] +// ------------------------------------------------------------------- +// prettier-ignore +export type TExprTermTailMapping + = Input +// prettier-ignore +export function ExprTermTailMapping(input: [unknown, unknown, unknown] | [], context: unknown) { + return input +} +// ------------------------------------------------------------------- +// ExprTerm: [Factor, ExprTermTail] +// ------------------------------------------------------------------- +// prettier-ignore +export type TExprTermMapping = ( + Input extends [infer Left extends T.TSchema, infer Rest extends unknown[]] + ? TExprBinaryMapping + : [] +) +// prettier-ignore +export function ExprTermMapping(input: [unknown, unknown], context: unknown) { + const [left, rest] = input as [T.TSchema, unknown[]] + return ExprBinaryMapping(left, rest) +} +// ------------------------------------------------------------------- +// ExprTail: ['|', ExprTerm, ExprTail] | [] +// ------------------------------------------------------------------- +// prettier-ignore +export type TExprTailMapping + = Input +// prettier-ignore +export function ExprTailMapping(input: [unknown, unknown, unknown] | [], context: unknown) { + return input +} +// ------------------------------------------------------------------- +// Expr: [ExprTerm, ExprTail] +// ------------------------------------------------------------------- +// prettier-ignore +export type TExprMapping + = Input extends [infer Left extends T.TSchema, infer Rest extends unknown[]] + ? TExprBinaryMapping + : [] +// prettier-ignore +export function ExprMapping(input: [unknown, unknown], context: unknown) { + const [left, rest] = input as [T.TSchema, unknown[]] + return ExprBinaryMapping(left, rest) +} +// ------------------------------------------------------------------- +// Type: GenericArguments -> Expr | Expr +// ------------------------------------------------------------------- +// prettier-ignore +export type TTypeMapping + = Input +// prettier-ignore +export function TypeMapping(input: unknown, context: unknown) { + return input +} +// ------------------------------------------------------------------- +// PropertyKey: | +// ------------------------------------------------------------------- +// prettier-ignore +export type TPropertyKeyMapping + = Input +// prettier-ignore +export function PropertyKeyMapping(input: string, context: unknown) { + return input +} +// ------------------------------------------------------------------- +// Readonly: ['readonly'] | [] +// ------------------------------------------------------------------- +// prettier-ignore +export type TReadonlyMapping + = Input extends [unknown] ? true : false +// prettier-ignore +export function ReadonlyMapping(input: [unknown] | [], context: unknown) { + return input.length > 0 +} +// ------------------------------------------------------------------- +// Optional: ['?'] | [] +// ------------------------------------------------------------------- +// prettier-ignore +export type TOptionalMapping + = Input extends [unknown] ? true : false +// prettier-ignore +export function OptionalMapping(input: [unknown] | [], context: unknown) { + return input.length > 0 +} +// ------------------------------------------------------------------- +// Property: [Readonly, PropertyKey, Optional, ':', Type] +// ------------------------------------------------------------------- +// prettier-ignore +export type TPropertyMapping + = Input extends [infer IsReadonly extends boolean, infer Key extends string, infer IsOptional extends boolean, string, infer Type extends T.TSchema] ? { + [_ in Key]: ( + [IsReadonly, IsOptional] extends [true, true] ? T.TReadonlyOptional : + [IsReadonly, IsOptional] extends [true, false] ? T.TReadonly : + [IsReadonly, IsOptional] extends [false, true] ? T.TOptional : + Type + ) + } : never +// prettier-ignore +export function PropertyMapping(input: [unknown, unknown, unknown, unknown, unknown], context: unknown) { + const [isReadonly, key, isOptional, _colon, type] = input as [boolean, string, boolean, ':', T.TSchema] + return { + [key]: ( + isReadonly && isOptional ? T.ReadonlyOptional(type) : + isReadonly && !isOptional ? T.Readonly(type) : + !isReadonly && isOptional ? T.Optional(type) : + type + ) + } +} +// ------------------------------------------------------------------- +// PropertyDelimiter: [',', '\n'] | [';', '\n'] | [','] | [';'] | ['\n'] +// ------------------------------------------------------------------- +// prettier-ignore +export type TPropertyDelimiterMapping + = Input +// prettier-ignore +export function PropertyDelimiterMapping(input: [unknown, unknown] | [unknown], context: unknown) { + return input +} +// ------------------------------------------------------------------- +// PropertyList: [[Property, PropertyDelimiter][], [Property] | []] +// ------------------------------------------------------------------- +// prettier-ignore +export type TPropertyListMapping + = TDelimited +// prettier-ignore +export function PropertyListMapping(input: [unknown, unknown], context: unknown) { + return Delimited(input) +} +// ------------------------------------------------------------------- +// Object: ['{', PropertyList, '}'] +// ------------------------------------------------------------------- +// prettier-ignore +type TObjectMappingReduce = ( + PropertiesList extends [infer Left extends T.TProperties, ...infer Right extends T.TProperties[]] + ? TObjectMappingReduce + : { [Key in keyof Result]: Result[Key] } +) +// prettier-ignore +export type TObjectMapping = + Input extends ['{', infer PropertyList extends T.TProperties[], '}'] + ? T.TObject> + : never +// prettier-ignore +export function ObjectMapping(input: [unknown, unknown, unknown], context: unknown) { + const propertyList = input[1] as T.TProperties[] + return T.Object(propertyList.reduce((result, property) => { + return { ...result, ...property } + }, {} as T.TProperties)) +} +// ------------------------------------------------------------------- +// ElementList: [[Type, ','][], [Type] | []] +// ------------------------------------------------------------------- +// prettier-ignore +export type TElementListMapping + = TDelimited +// prettier-ignore +export function ElementListMapping(input: [unknown, unknown], context: unknown) { + return Delimited(input) +} +// ------------------------------------------------------------------- +// Tuple: ['[', ElementList, ']'] +// ------------------------------------------------------------------- +// prettier-ignore +export type TTupleMapping + = Input extends ['[', infer Types extends T.TSchema[], ']'] ? T.TTuple : never +// prettier-ignore +export function TupleMapping(input: [unknown, unknown, unknown], context: unknown) { + return T.Tuple(input[1] as T.TSchema[]) +} +// ------------------------------------------------------------------- +// Parameter: [, ':', Type] +// ------------------------------------------------------------------- +// prettier-ignore +export type TParameterMapping + = Input extends [string, ':', infer Type extends T.TSchema] ? Type : never +// prettier-ignore +export function ParameterMapping(input: [unknown, unknown, unknown], context: unknown) { + const [_ident, _colon, type] = input as [string, ':', T.TSchema] + return type +} +// ------------------------------------------------------------------- +// ParameterList: [[Parameter, ','][], [Parameter] | []] +// ------------------------------------------------------------------- +// prettier-ignore +export type TParameterListMapping + = TDelimited +// prettier-ignore +export function ParameterListMapping(input: [unknown, unknown], context: unknown) { + return Delimited(input) +} +// ------------------------------------------------------------------- +// Function: ['(', ParameterList, ')', '=>', Type] +// ------------------------------------------------------------------- +// prettier-ignore +export type TFunctionMapping + = Input extends ['(', infer ParameterList extends T.TSchema[], ')', '=>', infer ReturnType extends T.TSchema] + ? T.TFunction + : never +// prettier-ignore +export function FunctionMapping(input: [unknown, unknown, unknown, unknown, unknown], context: unknown) { + const [_lparan, parameterList, _rparan, _arrow, returnType] = input as ['(', T.TSchema[], ')', '=>', T.TSchema] + return T.Function(parameterList, returnType) +} +// ------------------------------------------------------------------- +// Constructor: ['new', '(', ParameterList, ')', '=>', Type] +// ------------------------------------------------------------------- +// prettier-ignore +export type TConstructorMapping + = Input extends ['new', '(', infer ParameterList extends T.TSchema[], ')', '=>', infer InstanceType extends T.TSchema] + ? T.TConstructor + : never +// prettier-ignore +export function ConstructorMapping(input: [unknown, unknown, unknown, unknown, unknown, unknown], context: unknown) { + const [_new, _lparan, parameterList, _rparan, _arrow, instanceType] = input as ['new', '(', T.TSchema[], ')', '=>', T.TSchema] + return T.Constructor(parameterList, instanceType) +} +// ------------------------------------------------------------------- +// Mapped: ['{', '[', , 'in', Type, ']', ':', Type, '}'] +// ------------------------------------------------------------------- +// prettier-ignore +export type TMappedMapping + = Input extends ['{', '[', infer _Key extends string, 'in', infer _Right extends T.TSchema, ']', ':', infer _Type extends T.TSchema, '}'] + ? T.TLiteral<'Mapped types not supported'> + : never +// prettier-ignore +export function MappedMapping(input: [unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown], context: unknown) { + const [_lbrace, _lbracket, _key, _in, _right, _rbracket, _colon, _type] = input as ['{', '[', string, 'in', T.TSchema, ']', ':', T.TSchema, '}'] + return T.Literal('Mapped types not supported') +} +// ------------------------------------------------------------------- +// AsyncIterator: ['AsyncIterator', '<', Type, '>'] +// ------------------------------------------------------------------- +// prettier-ignore +export type TAsyncIteratorMapping + = Input extends ['AsyncIterator', '<', infer Type extends T.TSchema, '>'] + ? T.TAsyncIterator + : never +// prettier-ignore +export function AsyncIteratorMapping(input: [unknown, unknown, unknown, unknown], context: unknown) { + const [_name, _langle, type, _rangle] = input as ['AsyncIterator', '<', T.TSchema, '>'] + return T.AsyncIterator(type) +} +// ------------------------------------------------------------------- +// Iterator: ['Iterator', '<', Type, '>'] +// ------------------------------------------------------------------- +// prettier-ignore +export type TIteratorMapping + = Input extends ['Iterator', '<', infer Type extends T.TSchema, '>'] + ? T.TIterator + : never +// prettier-ignore +export function IteratorMapping(input: [unknown, unknown, unknown, unknown], context: unknown) { + const [_name, _langle, type, _rangle] = input as ['Iterator', '<', T.TSchema, '>'] + return T.Iterator(type) +} +// ------------------------------------------------------------------- +// Argument: ['Argument', '<', Type, '>'] +// ------------------------------------------------------------------- +// prettier-ignore +export type TArgumentMapping + = Input extends ['Argument', '<', infer Type extends T.TSchema, '>'] + ? Type extends T.TLiteral + ? T.TArgument + : T.TNever + : never +// prettier-ignore +export function ArgumentMapping(input: [unknown, unknown, unknown, unknown], context: unknown) { + return T.KindGuard.IsLiteralNumber(input[2]) + ? T.Argument(Math.trunc(input[2].const)) + : T.Never() +} +// ------------------------------------------------------------------- +// Awaited: ['Awaited', '<', Type, '>'] +// ------------------------------------------------------------------- +// prettier-ignore +export type TAwaitedMapping + = Input extends ['Awaited', '<', infer Type extends T.TSchema, '>'] + ? T.TAwaited + : never +// prettier-ignore +export function AwaitedMapping(input: [unknown, unknown, unknown, unknown], context: unknown) { + const [_name, _langle, type, _rangle] = input as ['Awaited', '<', T.TSchema, '>'] + return T.Awaited(type) +} +// ------------------------------------------------------------------- +// Array: ['Array', '<', Type, '>'] +// ------------------------------------------------------------------- +// prettier-ignore +export type TArrayMapping + = Input extends ['Array', '<', infer Type extends T.TSchema, '>'] + ? T.TArray + : never +// prettier-ignore +export function ArrayMapping(input: [unknown, unknown, unknown, unknown], context: unknown) { + const [_name, _langle, type, _rangle] = input as ['Array', '<', T.TSchema, '>'] + return T.Array(type) +} +// ------------------------------------------------------------------- +// Record: ['Record', '<', Type, ',', Type, '>'] +// ------------------------------------------------------------------- +// prettier-ignore +export type TRecordMapping + = Input extends ['Record', '<', infer Key extends T.TSchema, ',', infer Type extends T.TSchema, '>'] + ? T.TRecordOrObject + : never +// prettier-ignore +export function RecordMapping(input: [unknown, unknown, unknown, unknown, unknown, unknown], context: unknown) { + const [_name, _langle, key, _comma, type, _rangle] = input as ['Record', '<', T.TSchema, ',', T.TSchema, '>'] + return T.Record(key, type) +} +// ------------------------------------------------------------------- +// Promise: ['Promise', '<', Type, '>'] +// ------------------------------------------------------------------- +// prettier-ignore +export type TPromiseMapping + = Input extends ['Promise', '<', infer Type extends T.TSchema, '>'] + ? T.TPromise + : never +// prettier-ignore +export function PromiseMapping(input: [unknown, unknown, unknown, unknown], context: unknown) { + const [_name, _langle, type, _rangle] = input as ['Promise', '<', T.TSchema, '>'] + return T.Promise(type) +} +// ------------------------------------------------------------------- +// ConstructorParameters: ['ConstructorParameters', '<', Type, '>'] +// ------------------------------------------------------------------- +// prettier-ignore +export type TConstructorParametersMapping + = Input extends ['ConstructorParameters', '<', infer Type extends T.TSchema, '>'] + ? T.TConstructorParameters + : never +// prettier-ignore +export function ConstructorParametersMapping(input: [unknown, unknown, unknown, unknown], context: unknown) { + const [_name, _langle, type, _rangle] = input as ['ConstructorParameters', '<', T.TSchema, '>'] + return T.ConstructorParameters(type) +} +// ------------------------------------------------------------------- +// FunctionParameters: ['Parameters', '<', Type, '>'] +// ------------------------------------------------------------------- +// prettier-ignore +export type TFunctionParametersMapping + = Input extends ['Parameters', '<', infer Type extends T.TSchema, '>'] + ? T.TParameters + : never +// prettier-ignore +export function FunctionParametersMapping(input: [unknown, unknown, unknown, unknown], context: unknown) { + const [_name, _langle, type, _rangle] = input as ['Parameters', '<', T.TSchema, '>'] + return T.Parameters(type) +} +// ------------------------------------------------------------------- +// InstanceType: ['InstanceType', '<', Type, '>'] +// ------------------------------------------------------------------- +// prettier-ignore +export type TInstanceTypeMapping + = Input extends ['InstanceType', '<', infer Type extends T.TSchema, '>'] + ? T.TInstanceType + : never +// prettier-ignore +export function InstanceTypeMapping(input: [unknown, unknown, unknown, unknown], context: unknown) { + const [_name, _langle, type, _rangle] = input as ['InstanceType', '<', T.TSchema, '>'] + return T.InstanceType(type) +} +// ------------------------------------------------------------------- +// ReturnType: ['ReturnType', '<', Type, '>'] +// ------------------------------------------------------------------- +// prettier-ignore +export type TReturnTypeMapping + = Input extends ['ReturnType', '<', infer Type extends T.TSchema, '>'] + ? T.TReturnType + : never +// prettier-ignore +export function ReturnTypeMapping(input: [unknown, unknown, unknown, unknown], context: unknown) { + const [_name, _langle, type, _rangle] = input as ['ReturnType', '<', T.TSchema, '>'] + return T.ReturnType(type) +} +// ------------------------------------------------------------------- +// Partial: ['Partial', '<', Type, '>'] +// ------------------------------------------------------------------- +// prettier-ignore +export type TPartialMapping + = Input extends ['Partial', '<', infer Type extends T.TSchema, '>'] + ? T.TPartial + : never +// prettier-ignore +export function PartialMapping(input: [unknown, unknown, unknown, unknown], context: unknown) { + const [_name, _langle, type, _rangle] = input as ['Partial', '<', T.TSchema, '>'] + return T.Partial(type) +} +// ------------------------------------------------------------------- +// Required: ['Required', '<', Type, '>'] +// ------------------------------------------------------------------- +// prettier-ignore +export type TRequiredMapping + = Input extends ['Required', '<', infer Type extends T.TSchema, '>'] + ? T.TRequired + : never +// prettier-ignore +export function RequiredMapping(input: [unknown, unknown, unknown, unknown], context: unknown) { + const [_name, _langle, type, _rangle] = input as ['Required', '<', T.TSchema, '>'] + return T.Required(type) +} +// ------------------------------------------------------------------- +// Pick: ['Pick', '<', Type, ',', Type, '>'] +// ------------------------------------------------------------------- +// prettier-ignore +export type TPickMapping + = Input extends ['Pick', '<', infer Type extends T.TSchema, ',', infer Key extends T.TSchema, '>'] + ? T.TPick + : never +// prettier-ignore +export function PickMapping(input: [unknown, unknown, unknown, unknown, unknown, unknown], context: unknown) { + const [_name, _langle, key, _comma, type, _rangle] = input as ['Pick', '<', T.TSchema, ',', T.TSchema, '>'] + return T.Pick(key, type) +} +// ------------------------------------------------------------------- +// Omit: ['Omit', '<', Type, ',', Type, '>'] +// ------------------------------------------------------------------- +// prettier-ignore +export type TOmitMapping + = Input extends ['Omit', '<', infer Type extends T.TSchema, ',', infer Key extends T.TSchema, '>'] + ? T.TOmit + : never +// prettier-ignore +export function OmitMapping(input: [unknown, unknown, unknown, unknown, unknown, unknown], context: unknown) { + const [_name, _langle, key, _comma, type, _rangle] = input as ['Omit', '<', T.TSchema, ',', T.TSchema, '>'] + return T.Omit(key, type) +} +// ------------------------------------------------------------------- +// Exclude: ['Exclude', '<', Type, ',', Type, '>'] +// ------------------------------------------------------------------- +// prettier-ignore +export type TExcludeMapping + = Input extends ['Exclude', '<', infer Type extends T.TSchema, ',', infer Key extends T.TSchema, '>'] + ? T.TExclude + : never +// prettier-ignore +export function ExcludeMapping(input: [unknown, unknown, unknown, unknown, unknown, unknown], context: unknown) { + const [_name, _langle, key, _comma, type, _rangle] = input as ['Exclude', '<', T.TSchema, ',', T.TSchema, '>'] + return T.Exclude(key, type) +} +// ------------------------------------------------------------------- +// Extract: ['Extract', '<', Type, ',', Type, '>'] +// ------------------------------------------------------------------- +// prettier-ignore +export type TExtractMapping + = Input extends ['Extract', '<', infer Type extends T.TSchema, ',', infer Key extends T.TSchema, '>'] + ? T.TExtract + : never +// prettier-ignore +export function ExtractMapping(input: [unknown, unknown, unknown, unknown, unknown, unknown], context: unknown) { + const [_name, _langle, key, _comma, type, _rangle] = input as ['Extract', '<', T.TSchema, ',', T.TSchema, '>'] + return T.Extract(key, type) +} +// ------------------------------------------------------------------- +// Uppercase: ['Uppercase', '<', Type, '>'] +// ------------------------------------------------------------------- +// prettier-ignore +export type TUppercaseMapping + = Input extends ['Uppercase', '<', infer Type extends T.TSchema, '>'] + ? T.TUppercase + : never +// prettier-ignore +export function UppercaseMapping(input: [unknown, unknown, unknown, unknown], context: unknown) { + const [_name, _langle, type, _rangle] = input as ['Uppercase', '<', T.TSchema, '>'] + return T.Uppercase(type) +} +// ------------------------------------------------------------------- +// Lowercase: ['Lowercase', '<', Type, '>'] +// ------------------------------------------------------------------- +// prettier-ignore +export type TLowercaseMapping + = Input extends ['Lowercase', '<', infer Type extends T.TSchema, '>'] + ? T.TLowercase + : never +// prettier-ignore +export function LowercaseMapping(input: [unknown, unknown, unknown, unknown], context: unknown) { + const [_name, _langle, type, _rangle] = input as ['Lowercase', '<', T.TSchema, '>'] + return T.Lowercase(type) +} +// ------------------------------------------------------------------- +// Capitalize: ['Capitalize', '<', Type, '>'] +// ------------------------------------------------------------------- +// prettier-ignore +export type TCapitalizeMapping + = Input extends ['Capitalize', '<', infer Type extends T.TSchema, '>'] + ? T.TCapitalize + : never +// prettier-ignore +export function CapitalizeMapping(input: [unknown, unknown, unknown, unknown], context: unknown) { + const [_name, _langle, type, _rangle] = input as ['Capitalize', '<', T.TSchema, '>'] + return T.Capitalize(type) +} +// ------------------------------------------------------------------- +// Uncapitalize: ['Uncapitalize', '<', Type, '>'] +// ------------------------------------------------------------------- +// prettier-ignore +export type TUncapitalizeMapping + = Input extends ['Uncapitalize', '<', infer Type extends T.TSchema, '>'] + ? T.TUncapitalize + : never +// prettier-ignore +export function UncapitalizeMapping(input: [unknown, unknown, unknown, unknown], context: unknown) { + const [_name, _langle, type, _rangle] = input as ['Uncapitalize', '<', T.TSchema, '>'] + return T.Uncapitalize(type) +} +// ------------------------------------------------------------------- +// Date: 'Date' +// ------------------------------------------------------------------- +// prettier-ignore +export type TDateMapping + = T.TDate +// prettier-ignore +export function DateMapping(input: 'Date', context: unknown) { + return T.Date() +} +// ------------------------------------------------------------------- +// Uint8Array: 'Uint8Array' +// ------------------------------------------------------------------- +// prettier-ignore +export type TUint8ArrayMapping + = T.TUint8Array +// prettier-ignore +export function Uint8ArrayMapping(input: 'Uint8Array', context: unknown) { + return T.Uint8Array() +} +// ------------------------------------------------------------------- +// Reference: +// ------------------------------------------------------------------- +// prettier-ignore +export type TReferenceMapping + = Context extends T.TProperties + ? Input extends string + ? TDereference + : never + : never +// prettier-ignore +export function ReferenceMapping(input: string, context: unknown) { + const target = Dereference(context as T.TProperties, input) + return target +} diff --git a/src/syntax/parser.ts b/src/syntax/parser.ts new file mode 100644 index 000000000..6787f8588 --- /dev/null +++ b/src/syntax/parser.ts @@ -0,0 +1,1686 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/syntax + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { Runtime, Static } from '../parser/index' +import * as T from '../type/index' +import * as S from './mapping' + +// ------------------------------------------------------------------ +// Parser +// +// The following code is optimized inline types generated by a remote +// compiler process. It is readonly and should not be modified. +// ------------------------------------------------------------------ + +export type TGenericReferenceParameterList_0 = ( + TType extends [infer _0, infer Input extends string] ? (Static.Token.Const<',', Input> extends [infer _1, infer Input extends string] ? [[_0, _1], Input] : []) : [] +) extends [infer _0, infer Input extends string] + ? TGenericReferenceParameterList_0 + : [Result, Input] +export type TGenericReferenceParameterList = ( + TGenericReferenceParameterList_0 extends [infer _0, infer Input extends string] + ? ( + (TType extends [infer _0, infer Input extends string] ? [[_0], Input] : []) extends [infer _0, infer Input extends string] ? [_0, Input] : [[], Input] extends [infer _0, infer Input extends string] ? [_0, Input] : [] + ) extends [infer _1, infer Input extends string] + ? [[_0, _1], Input] + : [] + : [] +) extends [infer _0 extends [unknown, unknown], infer Input extends string] + ? [S.TGenericReferenceParameterListMapping<_0, Context>, Input] + : [] +export type TGenericReference = ( + Static.Token.Ident extends [infer _0, infer Input extends string] + ? Static.Token.Const<'<', Input> extends [infer _1, infer Input extends string] + ? TGenericReferenceParameterList extends [infer _2, infer Input extends string] + ? Static.Token.Const<'>', Input> extends [infer _3, infer Input extends string] + ? [[_0, _1, _2, _3], Input] + : [] + : [] + : [] + : [] +) extends [infer _0 extends [unknown, unknown, unknown, unknown], infer Input extends string] + ? [S.TGenericReferenceMapping<_0, Context>, Input] + : [] +export type TGenericArgumentsList_0 = ( + Static.Token.Ident extends [infer _0, infer Input extends string] ? (Static.Token.Const<',', Input> extends [infer _1, infer Input extends string] ? [[_0, _1], Input] : []) : [] +) extends [infer _0, infer Input extends string] + ? TGenericArgumentsList_0 + : [Result, Input] +export type TGenericArgumentsList = ( + TGenericArgumentsList_0 extends [infer _0, infer Input extends string] + ? ( + (Static.Token.Ident extends [infer _0, infer Input extends string] ? [[_0], Input] : []) extends [infer _0, infer Input extends string] + ? [_0, Input] + : [[], Input] extends [infer _0, infer Input extends string] + ? [_0, Input] + : [] + ) extends [infer _1, infer Input extends string] + ? [[_0, _1], Input] + : [] + : [] +) extends [infer _0 extends [unknown, unknown], infer Input extends string] + ? [S.TGenericArgumentsListMapping<_0, Context>, Input] + : [] +export type TGenericArguments = ( + Static.Token.Const<'<', Input> extends [infer _0, infer Input extends string] + ? TGenericArgumentsList extends [infer _1, infer Input extends string] + ? Static.Token.Const<'>', Input> extends [infer _2, infer Input extends string] + ? [[_0, _1, _2], Input] + : [] + : [] + : [] +) extends [infer _0 extends [unknown, unknown, unknown], infer Input extends string] + ? [S.TGenericArgumentsMapping<_0, Context>, Input] + : [] +export type TKeywordString = Static.Token.Const<'string', Input> extends [infer _0 extends 'string', infer Input extends string] ? [S.TKeywordStringMapping<_0, Context>, Input] : [] +export type TKeywordNumber = Static.Token.Const<'number', Input> extends [infer _0 extends 'number', infer Input extends string] ? [S.TKeywordNumberMapping<_0, Context>, Input] : [] +export type TKeywordBoolean = Static.Token.Const<'boolean', Input> extends [infer _0 extends 'boolean', infer Input extends string] + ? [S.TKeywordBooleanMapping<_0, Context>, Input] + : [] +export type TKeywordUndefined = Static.Token.Const<'undefined', Input> extends [infer _0 extends 'undefined', infer Input extends string] + ? [S.TKeywordUndefinedMapping<_0, Context>, Input] + : [] +export type TKeywordNull = Static.Token.Const<'null', Input> extends [infer _0 extends 'null', infer Input extends string] ? [S.TKeywordNullMapping<_0, Context>, Input] : [] +export type TKeywordInteger = Static.Token.Const<'integer', Input> extends [infer _0 extends 'integer', infer Input extends string] + ? [S.TKeywordIntegerMapping<_0, Context>, Input] + : [] +export type TKeywordBigInt = Static.Token.Const<'bigint', Input> extends [infer _0 extends 'bigint', infer Input extends string] ? [S.TKeywordBigIntMapping<_0, Context>, Input] : [] +export type TKeywordUnknown = Static.Token.Const<'unknown', Input> extends [infer _0 extends 'unknown', infer Input extends string] + ? [S.TKeywordUnknownMapping<_0, Context>, Input] + : [] +export type TKeywordAny = Static.Token.Const<'any', Input> extends [infer _0 extends 'any', infer Input extends string] ? [S.TKeywordAnyMapping<_0, Context>, Input] : [] +export type TKeywordNever = Static.Token.Const<'never', Input> extends [infer _0 extends 'never', infer Input extends string] ? [S.TKeywordNeverMapping<_0, Context>, Input] : [] +export type TKeywordSymbol = Static.Token.Const<'symbol', Input> extends [infer _0 extends 'symbol', infer Input extends string] ? [S.TKeywordSymbolMapping<_0, Context>, Input] : [] +export type TKeywordVoid = Static.Token.Const<'void', Input> extends [infer _0 extends 'void', infer Input extends string] ? [S.TKeywordVoidMapping<_0, Context>, Input] : [] +export type TKeyword = ( + TKeywordString extends [infer _0, infer Input extends string] + ? [_0, Input] + : TKeywordNumber extends [infer _0, infer Input extends string] + ? [_0, Input] + : TKeywordBoolean extends [infer _0, infer Input extends string] + ? [_0, Input] + : TKeywordUndefined extends [infer _0, infer Input extends string] + ? [_0, Input] + : TKeywordNull extends [infer _0, infer Input extends string] + ? [_0, Input] + : TKeywordInteger extends [infer _0, infer Input extends string] + ? [_0, Input] + : TKeywordBigInt extends [infer _0, infer Input extends string] + ? [_0, Input] + : TKeywordUnknown extends [infer _0, infer Input extends string] + ? [_0, Input] + : TKeywordAny extends [infer _0, infer Input extends string] + ? [_0, Input] + : TKeywordNever extends [infer _0, infer Input extends string] + ? [_0, Input] + : TKeywordSymbol extends [infer _0, infer Input extends string] + ? [_0, Input] + : TKeywordVoid extends [infer _0, infer Input extends string] + ? [_0, Input] + : [] +) extends [infer _0 extends unknown, infer Input extends string] + ? [S.TKeywordMapping<_0, Context>, Input] + : [] +export type TLiteralString = Static.Token.String<["'", '"', '`'], Input> extends [infer _0 extends string, infer Input extends string] + ? [S.TLiteralStringMapping<_0, Context>, Input] + : [] +export type TLiteralNumber = Static.Token.Number extends [infer _0 extends string, infer Input extends string] ? [S.TLiteralNumberMapping<_0, Context>, Input] : [] +export type TLiteralBoolean = ( + Static.Token.Const<'true', Input> extends [infer _0, infer Input extends string] ? [_0, Input] : Static.Token.Const<'false', Input> extends [infer _0, infer Input extends string] ? [_0, Input] : [] +) extends [infer _0 extends 'true' | 'false', infer Input extends string] + ? [S.TLiteralBooleanMapping<_0, Context>, Input] + : [] +export type TLiteral = ( + TLiteralBoolean extends [infer _0, infer Input extends string] + ? [_0, Input] + : TLiteralNumber extends [infer _0, infer Input extends string] + ? [_0, Input] + : TLiteralString extends [infer _0, infer Input extends string] + ? [_0, Input] + : [] +) extends [infer _0 extends unknown, infer Input extends string] + ? [S.TLiteralMapping<_0, Context>, Input] + : [] +export type TKeyOf = ( + (Static.Token.Const<'keyof', Input> extends [infer _0, infer Input extends string] ? [[_0], Input] : []) extends [infer _0, infer Input extends string] + ? [_0, Input] + : [[], Input] extends [infer _0, infer Input extends string] + ? [_0, Input] + : [] +) extends [infer _0 extends [unknown] | [], infer Input extends string] + ? [S.TKeyOfMapping<_0, Context>, Input] + : [] +export type TIndexArray_0 = ( + ( + Static.Token.Const<'[', Input> extends [infer _0, infer Input extends string] + ? TType extends [infer _1, infer Input extends string] + ? Static.Token.Const<']', Input> extends [infer _2, infer Input extends string] + ? [[_0, _1, _2], Input] + : [] + : [] + : [] + ) extends [infer _0, infer Input extends string] + ? [_0, Input] + : (Static.Token.Const<'[', Input> extends [infer _0, infer Input extends string] ? (Static.Token.Const<']', Input> extends [infer _1, infer Input extends string] ? [[_0, _1], Input] : []) : []) extends [ + infer _0, + infer Input extends string, + ] + ? [_0, Input] + : [] +) extends [infer _0, infer Input extends string] + ? TIndexArray_0 + : [Result, Input] +export type TIndexArray = TIndexArray_0 extends [infer _0 extends ([unknown, unknown, unknown] | [unknown, unknown])[], infer Input extends string] + ? [S.TIndexArrayMapping<_0, Context>, Input] + : [] +export type TExtends = ( + ( + Static.Token.Const<'extends', Input> extends [infer _0, infer Input extends string] + ? TType extends [infer _1, infer Input extends string] + ? Static.Token.Const<'?', Input> extends [infer _2, infer Input extends string] + ? TType extends [infer _3, infer Input extends string] + ? Static.Token.Const<':', Input> extends [infer _4, infer Input extends string] + ? TType extends [infer _5, infer Input extends string] + ? [[_0, _1, _2, _3, _4, _5], Input] + : [] + : [] + : [] + : [] + : [] + : [] + ) extends [infer _0, infer Input extends string] + ? [_0, Input] + : [[], Input] extends [infer _0, infer Input extends string] + ? [_0, Input] + : [] +) extends [infer _0 extends [unknown, unknown, unknown, unknown, unknown, unknown] | [], infer Input extends string] + ? [S.TExtendsMapping<_0, Context>, Input] + : [] +export type TBase = ( + ( + Static.Token.Const<'(', Input> extends [infer _0, infer Input extends string] + ? TType extends [infer _1, infer Input extends string] + ? Static.Token.Const<')', Input> extends [infer _2, infer Input extends string] + ? [[_0, _1, _2], Input] + : [] + : [] + : [] + ) extends [infer _0, infer Input extends string] + ? [_0, Input] + : TKeyword extends [infer _0, infer Input extends string] + ? [_0, Input] + : TObject extends [infer _0, infer Input extends string] + ? [_0, Input] + : TTuple extends [infer _0, infer Input extends string] + ? [_0, Input] + : TLiteral extends [infer _0, infer Input extends string] + ? [_0, Input] + : TConstructor extends [infer _0, infer Input extends string] + ? [_0, Input] + : TFunction extends [infer _0, infer Input extends string] + ? [_0, Input] + : TMapped extends [infer _0, infer Input extends string] + ? [_0, Input] + : TAsyncIterator extends [infer _0, infer Input extends string] + ? [_0, Input] + : TIterator extends [infer _0, infer Input extends string] + ? [_0, Input] + : TConstructorParameters extends [infer _0, infer Input extends string] + ? [_0, Input] + : TFunctionParameters extends [infer _0, infer Input extends string] + ? [_0, Input] + : TInstanceType extends [infer _0, infer Input extends string] + ? [_0, Input] + : TReturnType extends [infer _0, infer Input extends string] + ? [_0, Input] + : TArgument extends [infer _0, infer Input extends string] + ? [_0, Input] + : TAwaited extends [infer _0, infer Input extends string] + ? [_0, Input] + : TArray extends [infer _0, infer Input extends string] + ? [_0, Input] + : TRecord extends [infer _0, infer Input extends string] + ? [_0, Input] + : TPromise extends [infer _0, infer Input extends string] + ? [_0, Input] + : TPartial extends [infer _0, infer Input extends string] + ? [_0, Input] + : TRequired extends [infer _0, infer Input extends string] + ? [_0, Input] + : TPick extends [infer _0, infer Input extends string] + ? [_0, Input] + : TOmit extends [infer _0, infer Input extends string] + ? [_0, Input] + : TExclude extends [infer _0, infer Input extends string] + ? [_0, Input] + : TExtract extends [infer _0, infer Input extends string] + ? [_0, Input] + : TUppercase extends [infer _0, infer Input extends string] + ? [_0, Input] + : TLowercase extends [infer _0, infer Input extends string] + ? [_0, Input] + : TCapitalize extends [infer _0, infer Input extends string] + ? [_0, Input] + : TUncapitalize extends [infer _0, infer Input extends string] + ? [_0, Input] + : TDate extends [infer _0, infer Input extends string] + ? [_0, Input] + : TUint8Array extends [infer _0, infer Input extends string] + ? [_0, Input] + : TGenericReference extends [infer _0, infer Input extends string] + ? [_0, Input] + : TReference extends [infer _0, infer Input extends string] + ? [_0, Input] + : [] +) extends [infer _0 extends [unknown, unknown, unknown] | unknown, infer Input extends string] + ? [S.TBaseMapping<_0, Context>, Input] + : [] +export type TFactor = ( + TKeyOf extends [infer _0, infer Input extends string] + ? TBase extends [infer _1, infer Input extends string] + ? TIndexArray extends [infer _2, infer Input extends string] + ? TExtends extends [infer _3, infer Input extends string] + ? [[_0, _1, _2, _3], Input] + : [] + : [] + : [] + : [] +) extends [infer _0 extends [unknown, unknown, unknown, unknown], infer Input extends string] + ? [S.TFactorMapping<_0, Context>, Input] + : [] +export type TExprTermTail = ( + ( + Static.Token.Const<'&', Input> extends [infer _0, infer Input extends string] + ? TFactor extends [infer _1, infer Input extends string] + ? TExprTermTail extends [infer _2, infer Input extends string] + ? [[_0, _1, _2], Input] + : [] + : [] + : [] + ) extends [infer _0, infer Input extends string] + ? [_0, Input] + : [[], Input] extends [infer _0, infer Input extends string] + ? [_0, Input] + : [] +) extends [infer _0 extends [unknown, unknown, unknown] | [], infer Input extends string] + ? [S.TExprTermTailMapping<_0, Context>, Input] + : [] +export type TExprTerm = ( + TFactor extends [infer _0, infer Input extends string] ? (TExprTermTail extends [infer _1, infer Input extends string] ? [[_0, _1], Input] : []) : [] +) extends [infer _0 extends [unknown, unknown], infer Input extends string] + ? [S.TExprTermMapping<_0, Context>, Input] + : [] +export type TExprTail = ( + ( + Static.Token.Const<'|', Input> extends [infer _0, infer Input extends string] + ? TExprTerm extends [infer _1, infer Input extends string] + ? TExprTail extends [infer _2, infer Input extends string] + ? [[_0, _1, _2], Input] + : [] + : [] + : [] + ) extends [infer _0, infer Input extends string] + ? [_0, Input] + : [[], Input] extends [infer _0, infer Input extends string] + ? [_0, Input] + : [] +) extends [infer _0 extends [unknown, unknown, unknown] | [], infer Input extends string] + ? [S.TExprTailMapping<_0, Context>, Input] + : [] +export type TExpr = ( + TExprTerm extends [infer _0, infer Input extends string] ? (TExprTail extends [infer _1, infer Input extends string] ? [[_0, _1], Input] : []) : [] +) extends [infer _0 extends [unknown, unknown], infer Input extends string] + ? [S.TExprMapping<_0, Context>, Input] + : [] +export type TType = ( + TGenericArguments extends [infer _0 extends T.TProperties, infer Input extends string] + ? TExpr + : [] extends [infer _0, infer Input extends string] + ? [_0, Input] + : TExpr extends [infer _0, infer Input extends string] + ? [_0, Input] + : [] +) extends [infer _0 extends unknown, infer Input extends string] + ? [S.TTypeMapping<_0, Context>, Input] + : [] +export type TPropertyKey = ( + Static.Token.Ident extends [infer _0, infer Input extends string] ? [_0, Input] : Static.Token.String<["'", '"'], Input> extends [infer _0, infer Input extends string] ? [_0, Input] : [] +) extends [infer _0 extends string, infer Input extends string] + ? [S.TPropertyKeyMapping<_0, Context>, Input] + : [] +export type TReadonly = ( + (Static.Token.Const<'readonly', Input> extends [infer _0, infer Input extends string] ? [[_0], Input] : []) extends [infer _0, infer Input extends string] + ? [_0, Input] + : [[], Input] extends [infer _0, infer Input extends string] + ? [_0, Input] + : [] +) extends [infer _0 extends [unknown] | [], infer Input extends string] + ? [S.TReadonlyMapping<_0, Context>, Input] + : [] +export type TOptional = ( + (Static.Token.Const<'?', Input> extends [infer _0, infer Input extends string] ? [[_0], Input] : []) extends [infer _0, infer Input extends string] + ? [_0, Input] + : [[], Input] extends [infer _0, infer Input extends string] + ? [_0, Input] + : [] +) extends [infer _0 extends [unknown] | [], infer Input extends string] + ? [S.TOptionalMapping<_0, Context>, Input] + : [] +export type TProperty = ( + TReadonly extends [infer _0, infer Input extends string] + ? TPropertyKey extends [infer _1, infer Input extends string] + ? TOptional extends [infer _2, infer Input extends string] + ? Static.Token.Const<':', Input> extends [infer _3, infer Input extends string] + ? TType extends [infer _4, infer Input extends string] + ? [[_0, _1, _2, _3, _4], Input] + : [] + : [] + : [] + : [] + : [] +) extends [infer _0 extends [unknown, unknown, unknown, unknown, unknown], infer Input extends string] + ? [S.TPropertyMapping<_0, Context>, Input] + : [] +export type TPropertyDelimiter = ( + (Static.Token.Const<',', Input> extends [infer _0, infer Input extends string] ? (Static.Token.Const<'\n', Input> extends [infer _1, infer Input extends string] ? [[_0, _1], Input] : []) : []) extends [ + infer _0, + infer Input extends string, + ] + ? [_0, Input] + : (Static.Token.Const<';', Input> extends [infer _0, infer Input extends string] ? (Static.Token.Const<'\n', Input> extends [infer _1, infer Input extends string] ? [[_0, _1], Input] : []) : []) extends [ + infer _0, + infer Input extends string, + ] + ? [_0, Input] + : (Static.Token.Const<',', Input> extends [infer _0, infer Input extends string] ? [[_0], Input] : []) extends [infer _0, infer Input extends string] + ? [_0, Input] + : (Static.Token.Const<';', Input> extends [infer _0, infer Input extends string] ? [[_0], Input] : []) extends [infer _0, infer Input extends string] + ? [_0, Input] + : (Static.Token.Const<'\n', Input> extends [infer _0, infer Input extends string] ? [[_0], Input] : []) extends [infer _0, infer Input extends string] + ? [_0, Input] + : [] +) extends [infer _0 extends [unknown, unknown] | [unknown], infer Input extends string] + ? [S.TPropertyDelimiterMapping<_0, Context>, Input] + : [] +export type TPropertyList_0 = ( + TProperty extends [infer _0, infer Input extends string] ? (TPropertyDelimiter extends [infer _1, infer Input extends string] ? [[_0, _1], Input] : []) : [] +) extends [infer _0, infer Input extends string] + ? TPropertyList_0 + : [Result, Input] +export type TPropertyList = ( + TPropertyList_0 extends [infer _0, infer Input extends string] + ? ( + (TProperty extends [infer _0, infer Input extends string] ? [[_0], Input] : []) extends [infer _0, infer Input extends string] + ? [_0, Input] + : [[], Input] extends [infer _0, infer Input extends string] + ? [_0, Input] + : [] + ) extends [infer _1, infer Input extends string] + ? [[_0, _1], Input] + : [] + : [] +) extends [infer _0 extends [unknown, unknown], infer Input extends string] + ? [S.TPropertyListMapping<_0, Context>, Input] + : [] +export type TObject = ( + Static.Token.Const<'{', Input> extends [infer _0, infer Input extends string] + ? TPropertyList extends [infer _1, infer Input extends string] + ? Static.Token.Const<'}', Input> extends [infer _2, infer Input extends string] + ? [[_0, _1, _2], Input] + : [] + : [] + : [] +) extends [infer _0 extends [unknown, unknown, unknown], infer Input extends string] + ? [S.TObjectMapping<_0, Context>, Input] + : [] +export type TElementList_0 = ( + TType extends [infer _0, infer Input extends string] ? (Static.Token.Const<',', Input> extends [infer _1, infer Input extends string] ? [[_0, _1], Input] : []) : [] +) extends [infer _0, infer Input extends string] + ? TElementList_0 + : [Result, Input] +export type TElementList = ( + TElementList_0 extends [infer _0, infer Input extends string] + ? ( + (TType extends [infer _0, infer Input extends string] ? [[_0], Input] : []) extends [infer _0, infer Input extends string] ? [_0, Input] : [[], Input] extends [infer _0, infer Input extends string] ? [_0, Input] : [] + ) extends [infer _1, infer Input extends string] + ? [[_0, _1], Input] + : [] + : [] +) extends [infer _0 extends [unknown, unknown], infer Input extends string] + ? [S.TElementListMapping<_0, Context>, Input] + : [] +export type TTuple = ( + Static.Token.Const<'[', Input> extends [infer _0, infer Input extends string] + ? TElementList extends [infer _1, infer Input extends string] + ? Static.Token.Const<']', Input> extends [infer _2, infer Input extends string] + ? [[_0, _1, _2], Input] + : [] + : [] + : [] +) extends [infer _0 extends [unknown, unknown, unknown], infer Input extends string] + ? [S.TTupleMapping<_0, Context>, Input] + : [] +export type TParameter = ( + Static.Token.Ident extends [infer _0, infer Input extends string] + ? Static.Token.Const<':', Input> extends [infer _1, infer Input extends string] + ? TType extends [infer _2, infer Input extends string] + ? [[_0, _1, _2], Input] + : [] + : [] + : [] +) extends [infer _0 extends [unknown, unknown, unknown], infer Input extends string] + ? [S.TParameterMapping<_0, Context>, Input] + : [] +export type TParameterList_0 = ( + TParameter extends [infer _0, infer Input extends string] ? (Static.Token.Const<',', Input> extends [infer _1, infer Input extends string] ? [[_0, _1], Input] : []) : [] +) extends [infer _0, infer Input extends string] + ? TParameterList_0 + : [Result, Input] +export type TParameterList = ( + TParameterList_0 extends [infer _0, infer Input extends string] + ? ( + (TParameter extends [infer _0, infer Input extends string] ? [[_0], Input] : []) extends [infer _0, infer Input extends string] + ? [_0, Input] + : [[], Input] extends [infer _0, infer Input extends string] + ? [_0, Input] + : [] + ) extends [infer _1, infer Input extends string] + ? [[_0, _1], Input] + : [] + : [] +) extends [infer _0 extends [unknown, unknown], infer Input extends string] + ? [S.TParameterListMapping<_0, Context>, Input] + : [] +export type TFunction = ( + Static.Token.Const<'(', Input> extends [infer _0, infer Input extends string] + ? TParameterList extends [infer _1, infer Input extends string] + ? Static.Token.Const<')', Input> extends [infer _2, infer Input extends string] + ? Static.Token.Const<'=>', Input> extends [infer _3, infer Input extends string] + ? TType extends [infer _4, infer Input extends string] + ? [[_0, _1, _2, _3, _4], Input] + : [] + : [] + : [] + : [] + : [] +) extends [infer _0 extends [unknown, unknown, unknown, unknown, unknown], infer Input extends string] + ? [S.TFunctionMapping<_0, Context>, Input] + : [] +export type TConstructor = ( + Static.Token.Const<'new', Input> extends [infer _0, infer Input extends string] + ? Static.Token.Const<'(', Input> extends [infer _1, infer Input extends string] + ? TParameterList extends [infer _2, infer Input extends string] + ? Static.Token.Const<')', Input> extends [infer _3, infer Input extends string] + ? Static.Token.Const<'=>', Input> extends [infer _4, infer Input extends string] + ? TType extends [infer _5, infer Input extends string] + ? [[_0, _1, _2, _3, _4, _5], Input] + : [] + : [] + : [] + : [] + : [] + : [] +) extends [infer _0 extends [unknown, unknown, unknown, unknown, unknown, unknown], infer Input extends string] + ? [S.TConstructorMapping<_0, Context>, Input] + : [] +export type TMapped = ( + Static.Token.Const<'{', Input> extends [infer _0, infer Input extends string] + ? Static.Token.Const<'[', Input> extends [infer _1, infer Input extends string] + ? Static.Token.Ident extends [infer _2, infer Input extends string] + ? Static.Token.Const<'in', Input> extends [infer _3, infer Input extends string] + ? TType extends [infer _4, infer Input extends string] + ? Static.Token.Const<']', Input> extends [infer _5, infer Input extends string] + ? Static.Token.Const<':', Input> extends [infer _6, infer Input extends string] + ? TType extends [infer _7, infer Input extends string] + ? Static.Token.Const<'}', Input> extends [infer _8, infer Input extends string] + ? [[_0, _1, _2, _3, _4, _5, _6, _7, _8], Input] + : [] + : [] + : [] + : [] + : [] + : [] + : [] + : [] + : [] +) extends [infer _0 extends [unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown], infer Input extends string] + ? [S.TMappedMapping<_0, Context>, Input] + : [] +export type TAsyncIterator = ( + Static.Token.Const<'AsyncIterator', Input> extends [infer _0, infer Input extends string] + ? Static.Token.Const<'<', Input> extends [infer _1, infer Input extends string] + ? TType extends [infer _2, infer Input extends string] + ? Static.Token.Const<'>', Input> extends [infer _3, infer Input extends string] + ? [[_0, _1, _2, _3], Input] + : [] + : [] + : [] + : [] +) extends [infer _0 extends [unknown, unknown, unknown, unknown], infer Input extends string] + ? [S.TAsyncIteratorMapping<_0, Context>, Input] + : [] +export type TIterator = ( + Static.Token.Const<'Iterator', Input> extends [infer _0, infer Input extends string] + ? Static.Token.Const<'<', Input> extends [infer _1, infer Input extends string] + ? TType extends [infer _2, infer Input extends string] + ? Static.Token.Const<'>', Input> extends [infer _3, infer Input extends string] + ? [[_0, _1, _2, _3], Input] + : [] + : [] + : [] + : [] +) extends [infer _0 extends [unknown, unknown, unknown, unknown], infer Input extends string] + ? [S.TIteratorMapping<_0, Context>, Input] + : [] +export type TArgument = ( + Static.Token.Const<'Argument', Input> extends [infer _0, infer Input extends string] + ? Static.Token.Const<'<', Input> extends [infer _1, infer Input extends string] + ? TType extends [infer _2, infer Input extends string] + ? Static.Token.Const<'>', Input> extends [infer _3, infer Input extends string] + ? [[_0, _1, _2, _3], Input] + : [] + : [] + : [] + : [] +) extends [infer _0 extends [unknown, unknown, unknown, unknown], infer Input extends string] + ? [S.TArgumentMapping<_0, Context>, Input] + : [] +export type TAwaited = ( + Static.Token.Const<'Awaited', Input> extends [infer _0, infer Input extends string] + ? Static.Token.Const<'<', Input> extends [infer _1, infer Input extends string] + ? TType extends [infer _2, infer Input extends string] + ? Static.Token.Const<'>', Input> extends [infer _3, infer Input extends string] + ? [[_0, _1, _2, _3], Input] + : [] + : [] + : [] + : [] +) extends [infer _0 extends [unknown, unknown, unknown, unknown], infer Input extends string] + ? [S.TAwaitedMapping<_0, Context>, Input] + : [] +export type TArray = ( + Static.Token.Const<'Array', Input> extends [infer _0, infer Input extends string] + ? Static.Token.Const<'<', Input> extends [infer _1, infer Input extends string] + ? TType extends [infer _2, infer Input extends string] + ? Static.Token.Const<'>', Input> extends [infer _3, infer Input extends string] + ? [[_0, _1, _2, _3], Input] + : [] + : [] + : [] + : [] +) extends [infer _0 extends [unknown, unknown, unknown, unknown], infer Input extends string] + ? [S.TArrayMapping<_0, Context>, Input] + : [] +export type TRecord = ( + Static.Token.Const<'Record', Input> extends [infer _0, infer Input extends string] + ? Static.Token.Const<'<', Input> extends [infer _1, infer Input extends string] + ? TType extends [infer _2, infer Input extends string] + ? Static.Token.Const<',', Input> extends [infer _3, infer Input extends string] + ? TType extends [infer _4, infer Input extends string] + ? Static.Token.Const<'>', Input> extends [infer _5, infer Input extends string] + ? [[_0, _1, _2, _3, _4, _5], Input] + : [] + : [] + : [] + : [] + : [] + : [] +) extends [infer _0 extends [unknown, unknown, unknown, unknown, unknown, unknown], infer Input extends string] + ? [S.TRecordMapping<_0, Context>, Input] + : [] +export type TPromise = ( + Static.Token.Const<'Promise', Input> extends [infer _0, infer Input extends string] + ? Static.Token.Const<'<', Input> extends [infer _1, infer Input extends string] + ? TType extends [infer _2, infer Input extends string] + ? Static.Token.Const<'>', Input> extends [infer _3, infer Input extends string] + ? [[_0, _1, _2, _3], Input] + : [] + : [] + : [] + : [] +) extends [infer _0 extends [unknown, unknown, unknown, unknown], infer Input extends string] + ? [S.TPromiseMapping<_0, Context>, Input] + : [] +export type TConstructorParameters = ( + Static.Token.Const<'ConstructorParameters', Input> extends [infer _0, infer Input extends string] + ? Static.Token.Const<'<', Input> extends [infer _1, infer Input extends string] + ? TType extends [infer _2, infer Input extends string] + ? Static.Token.Const<'>', Input> extends [infer _3, infer Input extends string] + ? [[_0, _1, _2, _3], Input] + : [] + : [] + : [] + : [] +) extends [infer _0 extends [unknown, unknown, unknown, unknown], infer Input extends string] + ? [S.TConstructorParametersMapping<_0, Context>, Input] + : [] +export type TFunctionParameters = ( + Static.Token.Const<'Parameters', Input> extends [infer _0, infer Input extends string] + ? Static.Token.Const<'<', Input> extends [infer _1, infer Input extends string] + ? TType extends [infer _2, infer Input extends string] + ? Static.Token.Const<'>', Input> extends [infer _3, infer Input extends string] + ? [[_0, _1, _2, _3], Input] + : [] + : [] + : [] + : [] +) extends [infer _0 extends [unknown, unknown, unknown, unknown], infer Input extends string] + ? [S.TFunctionParametersMapping<_0, Context>, Input] + : [] +export type TInstanceType = ( + Static.Token.Const<'InstanceType', Input> extends [infer _0, infer Input extends string] + ? Static.Token.Const<'<', Input> extends [infer _1, infer Input extends string] + ? TType extends [infer _2, infer Input extends string] + ? Static.Token.Const<'>', Input> extends [infer _3, infer Input extends string] + ? [[_0, _1, _2, _3], Input] + : [] + : [] + : [] + : [] +) extends [infer _0 extends [unknown, unknown, unknown, unknown], infer Input extends string] + ? [S.TInstanceTypeMapping<_0, Context>, Input] + : [] +export type TReturnType = ( + Static.Token.Const<'ReturnType', Input> extends [infer _0, infer Input extends string] + ? Static.Token.Const<'<', Input> extends [infer _1, infer Input extends string] + ? TType extends [infer _2, infer Input extends string] + ? Static.Token.Const<'>', Input> extends [infer _3, infer Input extends string] + ? [[_0, _1, _2, _3], Input] + : [] + : [] + : [] + : [] +) extends [infer _0 extends [unknown, unknown, unknown, unknown], infer Input extends string] + ? [S.TReturnTypeMapping<_0, Context>, Input] + : [] +export type TPartial = ( + Static.Token.Const<'Partial', Input> extends [infer _0, infer Input extends string] + ? Static.Token.Const<'<', Input> extends [infer _1, infer Input extends string] + ? TType extends [infer _2, infer Input extends string] + ? Static.Token.Const<'>', Input> extends [infer _3, infer Input extends string] + ? [[_0, _1, _2, _3], Input] + : [] + : [] + : [] + : [] +) extends [infer _0 extends [unknown, unknown, unknown, unknown], infer Input extends string] + ? [S.TPartialMapping<_0, Context>, Input] + : [] +export type TRequired = ( + Static.Token.Const<'Required', Input> extends [infer _0, infer Input extends string] + ? Static.Token.Const<'<', Input> extends [infer _1, infer Input extends string] + ? TType extends [infer _2, infer Input extends string] + ? Static.Token.Const<'>', Input> extends [infer _3, infer Input extends string] + ? [[_0, _1, _2, _3], Input] + : [] + : [] + : [] + : [] +) extends [infer _0 extends [unknown, unknown, unknown, unknown], infer Input extends string] + ? [S.TRequiredMapping<_0, Context>, Input] + : [] +export type TPick = ( + Static.Token.Const<'Pick', Input> extends [infer _0, infer Input extends string] + ? Static.Token.Const<'<', Input> extends [infer _1, infer Input extends string] + ? TType extends [infer _2, infer Input extends string] + ? Static.Token.Const<',', Input> extends [infer _3, infer Input extends string] + ? TType extends [infer _4, infer Input extends string] + ? Static.Token.Const<'>', Input> extends [infer _5, infer Input extends string] + ? [[_0, _1, _2, _3, _4, _5], Input] + : [] + : [] + : [] + : [] + : [] + : [] +) extends [infer _0 extends [unknown, unknown, unknown, unknown, unknown, unknown], infer Input extends string] + ? [S.TPickMapping<_0, Context>, Input] + : [] +export type TOmit = ( + Static.Token.Const<'Omit', Input> extends [infer _0, infer Input extends string] + ? Static.Token.Const<'<', Input> extends [infer _1, infer Input extends string] + ? TType extends [infer _2, infer Input extends string] + ? Static.Token.Const<',', Input> extends [infer _3, infer Input extends string] + ? TType extends [infer _4, infer Input extends string] + ? Static.Token.Const<'>', Input> extends [infer _5, infer Input extends string] + ? [[_0, _1, _2, _3, _4, _5], Input] + : [] + : [] + : [] + : [] + : [] + : [] +) extends [infer _0 extends [unknown, unknown, unknown, unknown, unknown, unknown], infer Input extends string] + ? [S.TOmitMapping<_0, Context>, Input] + : [] +export type TExclude = ( + Static.Token.Const<'Exclude', Input> extends [infer _0, infer Input extends string] + ? Static.Token.Const<'<', Input> extends [infer _1, infer Input extends string] + ? TType extends [infer _2, infer Input extends string] + ? Static.Token.Const<',', Input> extends [infer _3, infer Input extends string] + ? TType extends [infer _4, infer Input extends string] + ? Static.Token.Const<'>', Input> extends [infer _5, infer Input extends string] + ? [[_0, _1, _2, _3, _4, _5], Input] + : [] + : [] + : [] + : [] + : [] + : [] +) extends [infer _0 extends [unknown, unknown, unknown, unknown, unknown, unknown], infer Input extends string] + ? [S.TExcludeMapping<_0, Context>, Input] + : [] +export type TExtract = ( + Static.Token.Const<'Extract', Input> extends [infer _0, infer Input extends string] + ? Static.Token.Const<'<', Input> extends [infer _1, infer Input extends string] + ? TType extends [infer _2, infer Input extends string] + ? Static.Token.Const<',', Input> extends [infer _3, infer Input extends string] + ? TType extends [infer _4, infer Input extends string] + ? Static.Token.Const<'>', Input> extends [infer _5, infer Input extends string] + ? [[_0, _1, _2, _3, _4, _5], Input] + : [] + : [] + : [] + : [] + : [] + : [] +) extends [infer _0 extends [unknown, unknown, unknown, unknown, unknown, unknown], infer Input extends string] + ? [S.TExtractMapping<_0, Context>, Input] + : [] +export type TUppercase = ( + Static.Token.Const<'Uppercase', Input> extends [infer _0, infer Input extends string] + ? Static.Token.Const<'<', Input> extends [infer _1, infer Input extends string] + ? TType extends [infer _2, infer Input extends string] + ? Static.Token.Const<'>', Input> extends [infer _3, infer Input extends string] + ? [[_0, _1, _2, _3], Input] + : [] + : [] + : [] + : [] +) extends [infer _0 extends [unknown, unknown, unknown, unknown], infer Input extends string] + ? [S.TUppercaseMapping<_0, Context>, Input] + : [] +export type TLowercase = ( + Static.Token.Const<'Lowercase', Input> extends [infer _0, infer Input extends string] + ? Static.Token.Const<'<', Input> extends [infer _1, infer Input extends string] + ? TType extends [infer _2, infer Input extends string] + ? Static.Token.Const<'>', Input> extends [infer _3, infer Input extends string] + ? [[_0, _1, _2, _3], Input] + : [] + : [] + : [] + : [] +) extends [infer _0 extends [unknown, unknown, unknown, unknown], infer Input extends string] + ? [S.TLowercaseMapping<_0, Context>, Input] + : [] +export type TCapitalize = ( + Static.Token.Const<'Capitalize', Input> extends [infer _0, infer Input extends string] + ? Static.Token.Const<'<', Input> extends [infer _1, infer Input extends string] + ? TType extends [infer _2, infer Input extends string] + ? Static.Token.Const<'>', Input> extends [infer _3, infer Input extends string] + ? [[_0, _1, _2, _3], Input] + : [] + : [] + : [] + : [] +) extends [infer _0 extends [unknown, unknown, unknown, unknown], infer Input extends string] + ? [S.TCapitalizeMapping<_0, Context>, Input] + : [] +export type TUncapitalize = ( + Static.Token.Const<'Uncapitalize', Input> extends [infer _0, infer Input extends string] + ? Static.Token.Const<'<', Input> extends [infer _1, infer Input extends string] + ? TType extends [infer _2, infer Input extends string] + ? Static.Token.Const<'>', Input> extends [infer _3, infer Input extends string] + ? [[_0, _1, _2, _3], Input] + : [] + : [] + : [] + : [] +) extends [infer _0 extends [unknown, unknown, unknown, unknown], infer Input extends string] + ? [S.TUncapitalizeMapping<_0, Context>, Input] + : [] +export type TDate = Static.Token.Const<'Date', Input> extends [infer _0 extends 'Date', infer Input extends string] ? [S.TDateMapping<_0, Context>, Input] : [] +export type TUint8Array = Static.Token.Const<'Uint8Array', Input> extends [infer _0 extends 'Uint8Array', infer Input extends string] + ? [S.TUint8ArrayMapping<_0, Context>, Input] + : [] +export type TReference = Static.Token.Ident extends [infer _0 extends string, infer Input extends string] ? [S.TReferenceMapping<_0, Context>, Input] : [] +const If = (result: [unknown, string] | [], left: (input: [unknown, string]) => [unknown, string] | [], right: () => [unknown, string] | [] = () => []): [unknown, string] | [] => (result.length === 2 ? left(result) : right()) + +export const GenericReferenceParameterList_0 = (input: string, context: T.TProperties, result: unknown[] = []): [unknown[], string] => + If( + If(Type(input, context), ([_0, input]) => If(Runtime.Token.Const(',', input), ([_1, input]) => [[_0, _1], input])), + ([_0, input]) => GenericReferenceParameterList_0(input, context, [...result, _0]), + () => [result, input], + ) as [unknown[], string] +export const GenericReferenceParameterList = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If(GenericReferenceParameterList_0(input, context), ([_0, input]) => + If( + If( + If(Type(input, context), ([_0, input]) => [[_0], input]), + ([_0, input]) => [_0, input], + () => + If( + [[], input], + ([_0, input]) => [_0, input], + () => [], + ), + ), + ([_1, input]) => [[_0, _1], input], + ), + ), + ([_0, input]) => [S.GenericReferenceParameterListMapping(_0 as [unknown, unknown], context), input], + ) +export const GenericReference = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If(Runtime.Token.Ident(input), ([_0, input]) => + If(Runtime.Token.Const('<', input), ([_1, input]) => If(GenericReferenceParameterList(input, context), ([_2, input]) => If(Runtime.Token.Const('>', input), ([_3, input]) => [[_0, _1, _2, _3], input]))), + ), + ([_0, input]) => [S.GenericReferenceMapping(_0 as [unknown, unknown, unknown, unknown], context), input], + ) +export const GenericArgumentsList_0 = (input: string, context: T.TProperties, result: unknown[] = []): [unknown[], string] => + If( + If(Runtime.Token.Ident(input), ([_0, input]) => If(Runtime.Token.Const(',', input), ([_1, input]) => [[_0, _1], input])), + ([_0, input]) => GenericArgumentsList_0(input, context, [...result, _0]), + () => [result, input], + ) as [unknown[], string] +export const GenericArgumentsList = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If(GenericArgumentsList_0(input, context), ([_0, input]) => + If( + If( + If(Runtime.Token.Ident(input), ([_0, input]) => [[_0], input]), + ([_0, input]) => [_0, input], + () => + If( + [[], input], + ([_0, input]) => [_0, input], + () => [], + ), + ), + ([_1, input]) => [[_0, _1], input], + ), + ), + ([_0, input]) => [S.GenericArgumentsListMapping(_0 as [unknown, unknown], context), input], + ) +export const GenericArguments = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If(Runtime.Token.Const('<', input), ([_0, input]) => If(GenericArgumentsList(input, context), ([_1, input]) => If(Runtime.Token.Const('>', input), ([_2, input]) => [[_0, _1, _2], input]))), + ([_0, input]) => [S.GenericArgumentsMapping(_0 as [unknown, unknown, unknown], context), input], + ) +export const KeywordString = (input: string, context: T.TProperties = {}): [unknown, string] | [] => If(Runtime.Token.Const('string', input), ([_0, input]) => [S.KeywordStringMapping(_0 as 'string', context), input]) +export const KeywordNumber = (input: string, context: T.TProperties = {}): [unknown, string] | [] => If(Runtime.Token.Const('number', input), ([_0, input]) => [S.KeywordNumberMapping(_0 as 'number', context), input]) +export const KeywordBoolean = (input: string, context: T.TProperties = {}): [unknown, string] | [] => If(Runtime.Token.Const('boolean', input), ([_0, input]) => [S.KeywordBooleanMapping(_0 as 'boolean', context), input]) +export const KeywordUndefined = (input: string, context: T.TProperties = {}): [unknown, string] | [] => If(Runtime.Token.Const('undefined', input), ([_0, input]) => [S.KeywordUndefinedMapping(_0 as 'undefined', context), input]) +export const KeywordNull = (input: string, context: T.TProperties = {}): [unknown, string] | [] => If(Runtime.Token.Const('null', input), ([_0, input]) => [S.KeywordNullMapping(_0 as 'null', context), input]) +export const KeywordInteger = (input: string, context: T.TProperties = {}): [unknown, string] | [] => If(Runtime.Token.Const('integer', input), ([_0, input]) => [S.KeywordIntegerMapping(_0 as 'integer', context), input]) +export const KeywordBigInt = (input: string, context: T.TProperties = {}): [unknown, string] | [] => If(Runtime.Token.Const('bigint', input), ([_0, input]) => [S.KeywordBigIntMapping(_0 as 'bigint', context), input]) +export const KeywordUnknown = (input: string, context: T.TProperties = {}): [unknown, string] | [] => If(Runtime.Token.Const('unknown', input), ([_0, input]) => [S.KeywordUnknownMapping(_0 as 'unknown', context), input]) +export const KeywordAny = (input: string, context: T.TProperties = {}): [unknown, string] | [] => If(Runtime.Token.Const('any', input), ([_0, input]) => [S.KeywordAnyMapping(_0 as 'any', context), input]) +export const KeywordNever = (input: string, context: T.TProperties = {}): [unknown, string] | [] => If(Runtime.Token.Const('never', input), ([_0, input]) => [S.KeywordNeverMapping(_0 as 'never', context), input]) +export const KeywordSymbol = (input: string, context: T.TProperties = {}): [unknown, string] | [] => If(Runtime.Token.Const('symbol', input), ([_0, input]) => [S.KeywordSymbolMapping(_0 as 'symbol', context), input]) +export const KeywordVoid = (input: string, context: T.TProperties = {}): [unknown, string] | [] => If(Runtime.Token.Const('void', input), ([_0, input]) => [S.KeywordVoidMapping(_0 as 'void', context), input]) +export const Keyword = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If( + KeywordString(input, context), + ([_0, input]) => [_0, input], + () => + If( + KeywordNumber(input, context), + ([_0, input]) => [_0, input], + () => + If( + KeywordBoolean(input, context), + ([_0, input]) => [_0, input], + () => + If( + KeywordUndefined(input, context), + ([_0, input]) => [_0, input], + () => + If( + KeywordNull(input, context), + ([_0, input]) => [_0, input], + () => + If( + KeywordInteger(input, context), + ([_0, input]) => [_0, input], + () => + If( + KeywordBigInt(input, context), + ([_0, input]) => [_0, input], + () => + If( + KeywordUnknown(input, context), + ([_0, input]) => [_0, input], + () => + If( + KeywordAny(input, context), + ([_0, input]) => [_0, input], + () => + If( + KeywordNever(input, context), + ([_0, input]) => [_0, input], + () => + If( + KeywordSymbol(input, context), + ([_0, input]) => [_0, input], + () => + If( + KeywordVoid(input, context), + ([_0, input]) => [_0, input], + () => [], + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ([_0, input]) => [S.KeywordMapping(_0 as unknown, context), input], + ) +export const LiteralString = (input: string, context: T.TProperties = {}): [unknown, string] | [] => If(Runtime.Token.String(["'", '"', '`'], input), ([_0, input]) => [S.LiteralStringMapping(_0 as string, context), input]) +export const LiteralNumber = (input: string, context: T.TProperties = {}): [unknown, string] | [] => If(Runtime.Token.Number(input), ([_0, input]) => [S.LiteralNumberMapping(_0 as string, context), input]) +export const LiteralBoolean = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If( + Runtime.Token.Const('true', input), + ([_0, input]) => [_0, input], + () => + If( + Runtime.Token.Const('false', input), + ([_0, input]) => [_0, input], + () => [], + ), + ), + ([_0, input]) => [S.LiteralBooleanMapping(_0 as 'true' | 'false', context), input], + ) +export const Literal = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If( + LiteralBoolean(input, context), + ([_0, input]) => [_0, input], + () => + If( + LiteralNumber(input, context), + ([_0, input]) => [_0, input], + () => + If( + LiteralString(input, context), + ([_0, input]) => [_0, input], + () => [], + ), + ), + ), + ([_0, input]) => [S.LiteralMapping(_0 as unknown, context), input], + ) +export const KeyOf = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If( + If(Runtime.Token.Const('keyof', input), ([_0, input]) => [[_0], input]), + ([_0, input]) => [_0, input], + () => + If( + [[], input], + ([_0, input]) => [_0, input], + () => [], + ), + ), + ([_0, input]) => [S.KeyOfMapping(_0 as [unknown] | [], context), input], + ) +export const IndexArray_0 = (input: string, context: T.TProperties, result: unknown[] = []): [unknown[], string] => + If( + If( + If(Runtime.Token.Const('[', input), ([_0, input]) => If(Type(input, context), ([_1, input]) => If(Runtime.Token.Const(']', input), ([_2, input]) => [[_0, _1, _2], input]))), + ([_0, input]) => [_0, input], + () => + If( + If(Runtime.Token.Const('[', input), ([_0, input]) => If(Runtime.Token.Const(']', input), ([_1, input]) => [[_0, _1], input])), + ([_0, input]) => [_0, input], + () => [], + ), + ), + ([_0, input]) => IndexArray_0(input, context, [...result, _0]), + () => [result, input], + ) as [unknown[], string] +export const IndexArray = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If(IndexArray_0(input, context), ([_0, input]) => [S.IndexArrayMapping(_0 as ([unknown, unknown, unknown] | [unknown, unknown])[], context), input]) +export const Extends = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If( + If(Runtime.Token.Const('extends', input), ([_0, input]) => + If(Type(input, context), ([_1, input]) => + If(Runtime.Token.Const('?', input), ([_2, input]) => If(Type(input, context), ([_3, input]) => If(Runtime.Token.Const(':', input), ([_4, input]) => If(Type(input, context), ([_5, input]) => [[_0, _1, _2, _3, _4, _5], input])))), + ), + ), + ([_0, input]) => [_0, input], + () => + If( + [[], input], + ([_0, input]) => [_0, input], + () => [], + ), + ), + ([_0, input]) => [S.ExtendsMapping(_0 as [unknown, unknown, unknown, unknown, unknown, unknown] | [], context), input], + ) +export const Base = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If( + If(Runtime.Token.Const('(', input), ([_0, input]) => If(Type(input, context), ([_1, input]) => If(Runtime.Token.Const(')', input), ([_2, input]) => [[_0, _1, _2], input]))), + ([_0, input]) => [_0, input], + () => + If( + Keyword(input, context), + ([_0, input]) => [_0, input], + () => + If( + _Object(input, context), + ([_0, input]) => [_0, input], + () => + If( + Tuple(input, context), + ([_0, input]) => [_0, input], + () => + If( + Literal(input, context), + ([_0, input]) => [_0, input], + () => + If( + Constructor(input, context), + ([_0, input]) => [_0, input], + () => + If( + Function(input, context), + ([_0, input]) => [_0, input], + () => + If( + Mapped(input, context), + ([_0, input]) => [_0, input], + () => + If( + AsyncIterator(input, context), + ([_0, input]) => [_0, input], + () => + If( + Iterator(input, context), + ([_0, input]) => [_0, input], + () => + If( + ConstructorParameters(input, context), + ([_0, input]) => [_0, input], + () => + If( + FunctionParameters(input, context), + ([_0, input]) => [_0, input], + () => + If( + InstanceType(input, context), + ([_0, input]) => [_0, input], + () => + If( + ReturnType(input, context), + ([_0, input]) => [_0, input], + () => + If( + Argument(input, context), + ([_0, input]) => [_0, input], + () => + If( + Awaited(input, context), + ([_0, input]) => [_0, input], + () => + If( + Array(input, context), + ([_0, input]) => [_0, input], + () => + If( + Record(input, context), + ([_0, input]) => [_0, input], + () => + If( + Promise(input, context), + ([_0, input]) => [_0, input], + () => + If( + Partial(input, context), + ([_0, input]) => [_0, input], + () => + If( + Required(input, context), + ([_0, input]) => [_0, input], + () => + If( + Pick(input, context), + ([_0, input]) => [_0, input], + () => + If( + Omit(input, context), + ([_0, input]) => [_0, input], + () => + If( + Exclude(input, context), + ([_0, input]) => [_0, input], + () => + If( + Extract(input, context), + ([_0, input]) => [_0, input], + () => + If( + Uppercase(input, context), + ([_0, input]) => [_0, input], + () => + If( + Lowercase(input, context), + ([_0, input]) => [_0, input], + () => + If( + Capitalize(input, context), + ([_0, input]) => [_0, input], + () => + If( + Uncapitalize(input, context), + ([_0, input]) => [_0, input], + () => + If( + Date(input, context), + ([_0, input]) => [_0, input], + () => + If( + Uint8Array(input, context), + ([_0, input]) => [_0, input], + () => + If( + GenericReference(input, context), + ([_0, input]) => [_0, input], + () => + If( + Reference(input, context), + ([_0, input]) => [_0, input], + () => [], + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ([_0, input]) => [S.BaseMapping(_0 as [unknown, unknown, unknown] | unknown, context), input], + ) +export const Factor = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If(KeyOf(input, context), ([_0, input]) => If(Base(input, context), ([_1, input]) => If(IndexArray(input, context), ([_2, input]) => If(Extends(input, context), ([_3, input]) => [[_0, _1, _2, _3], input])))), + ([_0, input]) => [S.FactorMapping(_0 as [unknown, unknown, unknown, unknown], context), input], + ) +export const ExprTermTail = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If( + If(Runtime.Token.Const('&', input), ([_0, input]) => If(Factor(input, context), ([_1, input]) => If(ExprTermTail(input, context), ([_2, input]) => [[_0, _1, _2], input]))), + ([_0, input]) => [_0, input], + () => + If( + [[], input], + ([_0, input]) => [_0, input], + () => [], + ), + ), + ([_0, input]) => [S.ExprTermTailMapping(_0 as [unknown, unknown, unknown] | [], context), input], + ) +export const ExprTerm = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If(Factor(input, context), ([_0, input]) => If(ExprTermTail(input, context), ([_1, input]) => [[_0, _1], input])), + ([_0, input]) => [S.ExprTermMapping(_0 as [unknown, unknown], context), input], + ) +export const ExprTail = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If( + If(Runtime.Token.Const('|', input), ([_0, input]) => If(ExprTerm(input, context), ([_1, input]) => If(ExprTail(input, context), ([_2, input]) => [[_0, _1, _2], input]))), + ([_0, input]) => [_0, input], + () => + If( + [[], input], + ([_0, input]) => [_0, input], + () => [], + ), + ), + ([_0, input]) => [S.ExprTailMapping(_0 as [unknown, unknown, unknown] | [], context), input], + ) +export const Expr = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If(ExprTerm(input, context), ([_0, input]) => If(ExprTail(input, context), ([_1, input]) => [[_0, _1], input])), + ([_0, input]) => [S.ExprMapping(_0 as [unknown, unknown], context), input], + ) +export const Type = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If( + If( + GenericArguments(input, context), + ([_0, input]) => Expr(input, _0 as T.TProperties), + () => [], + ), + ([_0, input]) => [_0, input], + () => + If( + Expr(input, context), + ([_0, input]) => [_0, input], + () => [], + ), + ), + ([_0, input]) => [S.TypeMapping(_0 as unknown, context), input], + ) +export const PropertyKey = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If( + Runtime.Token.Ident(input), + ([_0, input]) => [_0, input], + () => + If( + Runtime.Token.String(["'", '"'], input), + ([_0, input]) => [_0, input], + () => [], + ), + ), + ([_0, input]) => [S.PropertyKeyMapping(_0 as string, context), input], + ) +export const Readonly = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If( + If(Runtime.Token.Const('readonly', input), ([_0, input]) => [[_0], input]), + ([_0, input]) => [_0, input], + () => + If( + [[], input], + ([_0, input]) => [_0, input], + () => [], + ), + ), + ([_0, input]) => [S.ReadonlyMapping(_0 as [unknown] | [], context), input], + ) +export const Optional = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If( + If(Runtime.Token.Const('?', input), ([_0, input]) => [[_0], input]), + ([_0, input]) => [_0, input], + () => + If( + [[], input], + ([_0, input]) => [_0, input], + () => [], + ), + ), + ([_0, input]) => [S.OptionalMapping(_0 as [unknown] | [], context), input], + ) +export const Property = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If(Readonly(input, context), ([_0, input]) => + If(PropertyKey(input, context), ([_1, input]) => If(Optional(input, context), ([_2, input]) => If(Runtime.Token.Const(':', input), ([_3, input]) => If(Type(input, context), ([_4, input]) => [[_0, _1, _2, _3, _4], input])))), + ), + ([_0, input]) => [S.PropertyMapping(_0 as [unknown, unknown, unknown, unknown, unknown], context), input], + ) +export const PropertyDelimiter = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If( + If(Runtime.Token.Const(',', input), ([_0, input]) => If(Runtime.Token.Const('\n', input), ([_1, input]) => [[_0, _1], input])), + ([_0, input]) => [_0, input], + () => + If( + If(Runtime.Token.Const(';', input), ([_0, input]) => If(Runtime.Token.Const('\n', input), ([_1, input]) => [[_0, _1], input])), + ([_0, input]) => [_0, input], + () => + If( + If(Runtime.Token.Const(',', input), ([_0, input]) => [[_0], input]), + ([_0, input]) => [_0, input], + () => + If( + If(Runtime.Token.Const(';', input), ([_0, input]) => [[_0], input]), + ([_0, input]) => [_0, input], + () => + If( + If(Runtime.Token.Const('\n', input), ([_0, input]) => [[_0], input]), + ([_0, input]) => [_0, input], + () => [], + ), + ), + ), + ), + ), + ([_0, input]) => [S.PropertyDelimiterMapping(_0 as [unknown, unknown] | [unknown], context), input], + ) +export const PropertyList_0 = (input: string, context: T.TProperties, result: unknown[] = []): [unknown[], string] => + If( + If(Property(input, context), ([_0, input]) => If(PropertyDelimiter(input, context), ([_1, input]) => [[_0, _1], input])), + ([_0, input]) => PropertyList_0(input, context, [...result, _0]), + () => [result, input], + ) as [unknown[], string] +export const PropertyList = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If(PropertyList_0(input, context), ([_0, input]) => + If( + If( + If(Property(input, context), ([_0, input]) => [[_0], input]), + ([_0, input]) => [_0, input], + () => + If( + [[], input], + ([_0, input]) => [_0, input], + () => [], + ), + ), + ([_1, input]) => [[_0, _1], input], + ), + ), + ([_0, input]) => [S.PropertyListMapping(_0 as [unknown, unknown], context), input], + ) +export const _Object = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If(Runtime.Token.Const('{', input), ([_0, input]) => If(PropertyList(input, context), ([_1, input]) => If(Runtime.Token.Const('}', input), ([_2, input]) => [[_0, _1, _2], input]))), + ([_0, input]) => [S.ObjectMapping(_0 as [unknown, unknown, unknown], context), input], + ) +export const ElementList_0 = (input: string, context: T.TProperties, result: unknown[] = []): [unknown[], string] => + If( + If(Type(input, context), ([_0, input]) => If(Runtime.Token.Const(',', input), ([_1, input]) => [[_0, _1], input])), + ([_0, input]) => ElementList_0(input, context, [...result, _0]), + () => [result, input], + ) as [unknown[], string] +export const ElementList = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If(ElementList_0(input, context), ([_0, input]) => + If( + If( + If(Type(input, context), ([_0, input]) => [[_0], input]), + ([_0, input]) => [_0, input], + () => + If( + [[], input], + ([_0, input]) => [_0, input], + () => [], + ), + ), + ([_1, input]) => [[_0, _1], input], + ), + ), + ([_0, input]) => [S.ElementListMapping(_0 as [unknown, unknown], context), input], + ) +export const Tuple = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If(Runtime.Token.Const('[', input), ([_0, input]) => If(ElementList(input, context), ([_1, input]) => If(Runtime.Token.Const(']', input), ([_2, input]) => [[_0, _1, _2], input]))), + ([_0, input]) => [S.TupleMapping(_0 as [unknown, unknown, unknown], context), input], + ) +export const Parameter = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If(Runtime.Token.Ident(input), ([_0, input]) => If(Runtime.Token.Const(':', input), ([_1, input]) => If(Type(input, context), ([_2, input]) => [[_0, _1, _2], input]))), + ([_0, input]) => [S.ParameterMapping(_0 as [unknown, unknown, unknown], context), input], + ) +export const ParameterList_0 = (input: string, context: T.TProperties, result: unknown[] = []): [unknown[], string] => + If( + If(Parameter(input, context), ([_0, input]) => If(Runtime.Token.Const(',', input), ([_1, input]) => [[_0, _1], input])), + ([_0, input]) => ParameterList_0(input, context, [...result, _0]), + () => [result, input], + ) as [unknown[], string] +export const ParameterList = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If(ParameterList_0(input, context), ([_0, input]) => + If( + If( + If(Parameter(input, context), ([_0, input]) => [[_0], input]), + ([_0, input]) => [_0, input], + () => + If( + [[], input], + ([_0, input]) => [_0, input], + () => [], + ), + ), + ([_1, input]) => [[_0, _1], input], + ), + ), + ([_0, input]) => [S.ParameterListMapping(_0 as [unknown, unknown], context), input], + ) +export const Function = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If(Runtime.Token.Const('(', input), ([_0, input]) => + If(ParameterList(input, context), ([_1, input]) => If(Runtime.Token.Const(')', input), ([_2, input]) => If(Runtime.Token.Const('=>', input), ([_3, input]) => If(Type(input, context), ([_4, input]) => [[_0, _1, _2, _3, _4], input])))), + ), + ([_0, input]) => [S.FunctionMapping(_0 as [unknown, unknown, unknown, unknown, unknown], context), input], + ) +export const Constructor = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If(Runtime.Token.Const('new', input), ([_0, input]) => + If(Runtime.Token.Const('(', input), ([_1, input]) => + If(ParameterList(input, context), ([_2, input]) => + If(Runtime.Token.Const(')', input), ([_3, input]) => If(Runtime.Token.Const('=>', input), ([_4, input]) => If(Type(input, context), ([_5, input]) => [[_0, _1, _2, _3, _4, _5], input]))), + ), + ), + ), + ([_0, input]) => [S.ConstructorMapping(_0 as [unknown, unknown, unknown, unknown, unknown, unknown], context), input], + ) +export const Mapped = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If(Runtime.Token.Const('{', input), ([_0, input]) => + If(Runtime.Token.Const('[', input), ([_1, input]) => + If(Runtime.Token.Ident(input), ([_2, input]) => + If(Runtime.Token.Const('in', input), ([_3, input]) => + If(Type(input, context), ([_4, input]) => + If(Runtime.Token.Const(']', input), ([_5, input]) => + If(Runtime.Token.Const(':', input), ([_6, input]) => If(Type(input, context), ([_7, input]) => If(Runtime.Token.Const('}', input), ([_8, input]) => [[_0, _1, _2, _3, _4, _5, _6, _7, _8], input]))), + ), + ), + ), + ), + ), + ), + ([_0, input]) => [S.MappedMapping(_0 as [unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown], context), input], + ) +export const AsyncIterator = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If(Runtime.Token.Const('AsyncIterator', input), ([_0, input]) => + If(Runtime.Token.Const('<', input), ([_1, input]) => If(Type(input, context), ([_2, input]) => If(Runtime.Token.Const('>', input), ([_3, input]) => [[_0, _1, _2, _3], input]))), + ), + ([_0, input]) => [S.AsyncIteratorMapping(_0 as [unknown, unknown, unknown, unknown], context), input], + ) +export const Iterator = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If(Runtime.Token.Const('Iterator', input), ([_0, input]) => + If(Runtime.Token.Const('<', input), ([_1, input]) => If(Type(input, context), ([_2, input]) => If(Runtime.Token.Const('>', input), ([_3, input]) => [[_0, _1, _2, _3], input]))), + ), + ([_0, input]) => [S.IteratorMapping(_0 as [unknown, unknown, unknown, unknown], context), input], + ) +export const Argument = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If(Runtime.Token.Const('Argument', input), ([_0, input]) => + If(Runtime.Token.Const('<', input), ([_1, input]) => If(Type(input, context), ([_2, input]) => If(Runtime.Token.Const('>', input), ([_3, input]) => [[_0, _1, _2, _3], input]))), + ), + ([_0, input]) => [S.ArgumentMapping(_0 as [unknown, unknown, unknown, unknown], context), input], + ) +export const Awaited = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If(Runtime.Token.Const('Awaited', input), ([_0, input]) => + If(Runtime.Token.Const('<', input), ([_1, input]) => If(Type(input, context), ([_2, input]) => If(Runtime.Token.Const('>', input), ([_3, input]) => [[_0, _1, _2, _3], input]))), + ), + ([_0, input]) => [S.AwaitedMapping(_0 as [unknown, unknown, unknown, unknown], context), input], + ) +export const Array = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If(Runtime.Token.Const('Array', input), ([_0, input]) => If(Runtime.Token.Const('<', input), ([_1, input]) => If(Type(input, context), ([_2, input]) => If(Runtime.Token.Const('>', input), ([_3, input]) => [[_0, _1, _2, _3], input])))), + ([_0, input]) => [S.ArrayMapping(_0 as [unknown, unknown, unknown, unknown], context), input], + ) +export const Record = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If(Runtime.Token.Const('Record', input), ([_0, input]) => + If(Runtime.Token.Const('<', input), ([_1, input]) => + If(Type(input, context), ([_2, input]) => If(Runtime.Token.Const(',', input), ([_3, input]) => If(Type(input, context), ([_4, input]) => If(Runtime.Token.Const('>', input), ([_5, input]) => [[_0, _1, _2, _3, _4, _5], input])))), + ), + ), + ([_0, input]) => [S.RecordMapping(_0 as [unknown, unknown, unknown, unknown, unknown, unknown], context), input], + ) +export const Promise = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If(Runtime.Token.Const('Promise', input), ([_0, input]) => + If(Runtime.Token.Const('<', input), ([_1, input]) => If(Type(input, context), ([_2, input]) => If(Runtime.Token.Const('>', input), ([_3, input]) => [[_0, _1, _2, _3], input]))), + ), + ([_0, input]) => [S.PromiseMapping(_0 as [unknown, unknown, unknown, unknown], context), input], + ) +export const ConstructorParameters = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If(Runtime.Token.Const('ConstructorParameters', input), ([_0, input]) => + If(Runtime.Token.Const('<', input), ([_1, input]) => If(Type(input, context), ([_2, input]) => If(Runtime.Token.Const('>', input), ([_3, input]) => [[_0, _1, _2, _3], input]))), + ), + ([_0, input]) => [S.ConstructorParametersMapping(_0 as [unknown, unknown, unknown, unknown], context), input], + ) +export const FunctionParameters = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If(Runtime.Token.Const('Parameters', input), ([_0, input]) => + If(Runtime.Token.Const('<', input), ([_1, input]) => If(Type(input, context), ([_2, input]) => If(Runtime.Token.Const('>', input), ([_3, input]) => [[_0, _1, _2, _3], input]))), + ), + ([_0, input]) => [S.FunctionParametersMapping(_0 as [unknown, unknown, unknown, unknown], context), input], + ) +export const InstanceType = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If(Runtime.Token.Const('InstanceType', input), ([_0, input]) => + If(Runtime.Token.Const('<', input), ([_1, input]) => If(Type(input, context), ([_2, input]) => If(Runtime.Token.Const('>', input), ([_3, input]) => [[_0, _1, _2, _3], input]))), + ), + ([_0, input]) => [S.InstanceTypeMapping(_0 as [unknown, unknown, unknown, unknown], context), input], + ) +export const ReturnType = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If(Runtime.Token.Const('ReturnType', input), ([_0, input]) => + If(Runtime.Token.Const('<', input), ([_1, input]) => If(Type(input, context), ([_2, input]) => If(Runtime.Token.Const('>', input), ([_3, input]) => [[_0, _1, _2, _3], input]))), + ), + ([_0, input]) => [S.ReturnTypeMapping(_0 as [unknown, unknown, unknown, unknown], context), input], + ) +export const Partial = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If(Runtime.Token.Const('Partial', input), ([_0, input]) => + If(Runtime.Token.Const('<', input), ([_1, input]) => If(Type(input, context), ([_2, input]) => If(Runtime.Token.Const('>', input), ([_3, input]) => [[_0, _1, _2, _3], input]))), + ), + ([_0, input]) => [S.PartialMapping(_0 as [unknown, unknown, unknown, unknown], context), input], + ) +export const Required = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If(Runtime.Token.Const('Required', input), ([_0, input]) => + If(Runtime.Token.Const('<', input), ([_1, input]) => If(Type(input, context), ([_2, input]) => If(Runtime.Token.Const('>', input), ([_3, input]) => [[_0, _1, _2, _3], input]))), + ), + ([_0, input]) => [S.RequiredMapping(_0 as [unknown, unknown, unknown, unknown], context), input], + ) +export const Pick = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If(Runtime.Token.Const('Pick', input), ([_0, input]) => + If(Runtime.Token.Const('<', input), ([_1, input]) => + If(Type(input, context), ([_2, input]) => If(Runtime.Token.Const(',', input), ([_3, input]) => If(Type(input, context), ([_4, input]) => If(Runtime.Token.Const('>', input), ([_5, input]) => [[_0, _1, _2, _3, _4, _5], input])))), + ), + ), + ([_0, input]) => [S.PickMapping(_0 as [unknown, unknown, unknown, unknown, unknown, unknown], context), input], + ) +export const Omit = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If(Runtime.Token.Const('Omit', input), ([_0, input]) => + If(Runtime.Token.Const('<', input), ([_1, input]) => + If(Type(input, context), ([_2, input]) => If(Runtime.Token.Const(',', input), ([_3, input]) => If(Type(input, context), ([_4, input]) => If(Runtime.Token.Const('>', input), ([_5, input]) => [[_0, _1, _2, _3, _4, _5], input])))), + ), + ), + ([_0, input]) => [S.OmitMapping(_0 as [unknown, unknown, unknown, unknown, unknown, unknown], context), input], + ) +export const Exclude = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If(Runtime.Token.Const('Exclude', input), ([_0, input]) => + If(Runtime.Token.Const('<', input), ([_1, input]) => + If(Type(input, context), ([_2, input]) => If(Runtime.Token.Const(',', input), ([_3, input]) => If(Type(input, context), ([_4, input]) => If(Runtime.Token.Const('>', input), ([_5, input]) => [[_0, _1, _2, _3, _4, _5], input])))), + ), + ), + ([_0, input]) => [S.ExcludeMapping(_0 as [unknown, unknown, unknown, unknown, unknown, unknown], context), input], + ) +export const Extract = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If(Runtime.Token.Const('Extract', input), ([_0, input]) => + If(Runtime.Token.Const('<', input), ([_1, input]) => + If(Type(input, context), ([_2, input]) => If(Runtime.Token.Const(',', input), ([_3, input]) => If(Type(input, context), ([_4, input]) => If(Runtime.Token.Const('>', input), ([_5, input]) => [[_0, _1, _2, _3, _4, _5], input])))), + ), + ), + ([_0, input]) => [S.ExtractMapping(_0 as [unknown, unknown, unknown, unknown, unknown, unknown], context), input], + ) +export const Uppercase = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If(Runtime.Token.Const('Uppercase', input), ([_0, input]) => + If(Runtime.Token.Const('<', input), ([_1, input]) => If(Type(input, context), ([_2, input]) => If(Runtime.Token.Const('>', input), ([_3, input]) => [[_0, _1, _2, _3], input]))), + ), + ([_0, input]) => [S.UppercaseMapping(_0 as [unknown, unknown, unknown, unknown], context), input], + ) +export const Lowercase = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If(Runtime.Token.Const('Lowercase', input), ([_0, input]) => + If(Runtime.Token.Const('<', input), ([_1, input]) => If(Type(input, context), ([_2, input]) => If(Runtime.Token.Const('>', input), ([_3, input]) => [[_0, _1, _2, _3], input]))), + ), + ([_0, input]) => [S.LowercaseMapping(_0 as [unknown, unknown, unknown, unknown], context), input], + ) +export const Capitalize = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If(Runtime.Token.Const('Capitalize', input), ([_0, input]) => + If(Runtime.Token.Const('<', input), ([_1, input]) => If(Type(input, context), ([_2, input]) => If(Runtime.Token.Const('>', input), ([_3, input]) => [[_0, _1, _2, _3], input]))), + ), + ([_0, input]) => [S.CapitalizeMapping(_0 as [unknown, unknown, unknown, unknown], context), input], + ) +export const Uncapitalize = (input: string, context: T.TProperties = {}): [unknown, string] | [] => + If( + If(Runtime.Token.Const('Uncapitalize', input), ([_0, input]) => + If(Runtime.Token.Const('<', input), ([_1, input]) => If(Type(input, context), ([_2, input]) => If(Runtime.Token.Const('>', input), ([_3, input]) => [[_0, _1, _2, _3], input]))), + ), + ([_0, input]) => [S.UncapitalizeMapping(_0 as [unknown, unknown, unknown, unknown], context), input], + ) +export const Date = (input: string, context: T.TProperties = {}): [unknown, string] | [] => If(Runtime.Token.Const('Date', input), ([_0, input]) => [S.DateMapping(_0 as 'Date', context), input]) +export const Uint8Array = (input: string, context: T.TProperties = {}): [unknown, string] | [] => If(Runtime.Token.Const('Uint8Array', input), ([_0, input]) => [S.Uint8ArrayMapping(_0 as 'Uint8Array', context), input]) +export const Reference = (input: string, context: T.TProperties = {}): [unknown, string] | [] => If(Runtime.Token.Ident(input), ([_0, input]) => [S.ReferenceMapping(_0 as string, context), input]) diff --git a/src/syntax/syntax.ts b/src/syntax/syntax.ts new file mode 100644 index 000000000..82be0075c --- /dev/null +++ b/src/syntax/syntax.ts @@ -0,0 +1,62 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/syntax + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import * as t from '../type/index' +import { Type, TType } from './parser' + +// ------------------------------------------------------------------ +// NoInfer +// ------------------------------------------------------------------ +/** `[Experimental]` Parses type expressions into TypeBox types but does not infer */ +export function NoInfer, Input extends string>(context: Context, input: Input, options?: t.SchemaOptions): t.TSchema +/** `[Experimental]` Parses type expressions into TypeBox types but does not infer */ +export function NoInfer(input: Input, options?: t.SchemaOptions): t.TSchema +/** `[Experimental]` Parses type expressions into TypeBox types but does not infer */ +// prettier-ignore +export function NoInfer(...args: any[]): t.TSchema { + const withContext = typeof args[0] === 'string' ? false : true + const [context, code, options] = withContext ? [args[0], args[1], args[2] || {}] : [{}, args[0], args[1] || {}] + const result = Type(code, context)[0] + return t.KindGuard.IsSchema(result) + ? t.CloneType(result, options) + : t.Never(options) +} + +/** `[Experimental]` Parses type expressions into TypeBox types */ +// prettier-ignore +export type TSyntax, Code extends string> = ( + TType extends [infer Type extends t.TSchema, string] ? Type : t.TNever +) +/** `[Experimental]` Parses type expressions into TypeBox types */ +export function Syntax, Input extends string>(context: Context, input: Input, options?: t.SchemaOptions): TSyntax +/** `[Experimental]` Parses type expressions into TypeBox types */ +export function Syntax(annotation: Input, options?: t.SchemaOptions): TSyntax<{}, Input> +/** `[Experimental]` Parses type expressions into TypeBox types */ +export function Syntax(...args: any[]): never { + return NoInfer.apply(null, args as never) as never +} diff --git a/src/system/index.ts b/src/system/index.ts new file mode 100644 index 000000000..b9a805ba6 --- /dev/null +++ b/src/system/index.ts @@ -0,0 +1,30 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/system + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './policy' +export * from './system' diff --git a/src/system/policy.ts b/src/system/policy.ts new file mode 100644 index 000000000..88eb012c8 --- /dev/null +++ b/src/system/policy.ts @@ -0,0 +1,77 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/system + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { IsObject, IsArray, IsNumber, IsUndefined } from '../value/guard/index' + +export namespace TypeSystemPolicy { + // ------------------------------------------------------------------ + // TypeSystemPolicy: Instancing + // ------------------------------------------------------------------ + /** + * Configures the instantiation behavior of TypeBox types. The `default` option assigns raw JavaScript + * references for embedded types, which may cause side effects if type properties are explicitly updated + * outside the TypeBox type builder. The `clone` option creates copies of any shared types upon creation, + * preventing unintended side effects. The `freeze` option applies `Object.freeze()` to the type, making + * it fully readonly and immutable. Implementations should use `default` whenever possible, as it is the + * fastest way to instantiate types. The default setting is `default`. + */ + export let InstanceMode: 'default' | 'clone' | 'freeze' = 'default' + // ------------------------------------------------------------------ + // TypeSystemPolicy: Checking + // ------------------------------------------------------------------ + /** Sets whether TypeBox should assert optional properties using the TypeScript `exactOptionalPropertyTypes` assertion policy. The default is `false` */ + export let ExactOptionalPropertyTypes: boolean = false + /** Sets whether arrays should be treated as a kind of objects. The default is `false` */ + export let AllowArrayObject: boolean = false + /** Sets whether `NaN` or `Infinity` should be treated as valid numeric values. The default is `false` */ + export let AllowNaN: boolean = false + /** Sets whether `null` should validate for void types. The default is `false` */ + export let AllowNullVoid: boolean = false + /** Checks this value using the ExactOptionalPropertyTypes policy */ + export function IsExactOptionalProperty(value: Record, key: string) { + return ExactOptionalPropertyTypes ? key in value : value[key] !== undefined + } + /** Checks this value using the AllowArrayObjects policy */ + export function IsObjectLike(value: unknown): value is Record { + const isObject = IsObject(value) + return AllowArrayObject ? isObject : isObject && !IsArray(value) + } + /** Checks this value as a record using the AllowArrayObjects policy */ + export function IsRecordLike(value: unknown): value is Record { + return IsObjectLike(value) && !(value instanceof Date) && !(value instanceof Uint8Array) + } + /** Checks this value using the AllowNaN policy */ + export function IsNumberLike(value: unknown): value is number { + return AllowNaN ? IsNumber(value) : Number.isFinite(value) + } + /** Checks this value using the AllowVoidNull policy */ + export function IsVoidLike(value: unknown): value is void { + const isUndefined = IsUndefined(value) + return AllowNullVoid ? isUndefined || value === null : isUndefined + } +} diff --git a/src/system/system.ts b/src/system/system.ts new file mode 100644 index 000000000..b11de693d --- /dev/null +++ b/src/system/system.ts @@ -0,0 +1,66 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/system + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { TypeRegistry, FormatRegistry } from '../type/registry/index' +import { Unsafe, type TUnsafe } from '../type/unsafe/index' +import { Kind } from '../type/symbols/index' +import { TypeBoxError } from '../type/error/index' + +// ------------------------------------------------------------------ +// Errors +// ------------------------------------------------------------------ +export class TypeSystemDuplicateTypeKind extends TypeBoxError { + constructor(kind: string) { + super(`Duplicate type kind '${kind}' detected`) + } +} +export class TypeSystemDuplicateFormat extends TypeBoxError { + constructor(kind: string) { + super(`Duplicate string format '${kind}' detected`) + } +} +// ------------------------------------------------------------------ +// TypeSystem +// ------------------------------------------------------------------ +export type TypeFactoryFunction> = (options?: Partial) => TUnsafe + +/** Creates user defined types and formats and provides overrides for value checking behaviours */ +export namespace TypeSystem { + /** Creates a new type */ + export function Type>(kind: string, check: (options: Options, value: unknown) => boolean): TypeFactoryFunction { + if (TypeRegistry.Has(kind)) throw new TypeSystemDuplicateTypeKind(kind) + TypeRegistry.Set(kind, check) + return (options: Partial = {}) => Unsafe({ ...options, [Kind]: kind }) + } + /** Creates a new string format */ + export function Format(format: F, check: (value: string) => boolean): F { + if (FormatRegistry.Has(format)) throw new TypeSystemDuplicateFormat(format) + FormatRegistry.Set(format, check) + return format + } +} diff --git a/src/tsconfig.json b/src/tsconfig.json index 8440ad2e5..e65c06406 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -1,14 +1,4 @@ { - "compilerOptions": { - "strict": true, - "module": "CommonJS", - "target": "ESNext", - "moduleResolution": "node", - "lib": ["ESNext"], - "declaration": true, - "outDir": "../dist/", - }, - "files": [ - "typebox.ts" - ] -} \ No newline at end of file + "extends": "../tsconfig.json", + "files": ["compiler/index.ts", "errors/index.ts", "parser/index.ts", "syntax/index.ts", "system/index.ts", "type/index.ts", "value/index.ts", "index.ts"] +} diff --git a/src/type/any/any.ts b/src/type/any/any.ts new file mode 100644 index 000000000..8adc89cca --- /dev/null +++ b/src/type/any/any.ts @@ -0,0 +1,41 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { CreateType } from '../create/index' +import type { TSchema, SchemaOptions } from '../schema/index' +import { Kind } from '../symbols/index' + +export interface TAny extends TSchema { + [Kind]: 'Any' + static: any +} + +/** `[Json]` Creates an Any type */ +export function Any(options?: SchemaOptions): TAny { + return CreateType({ [Kind]: 'Any' }, options) as never +} diff --git a/src/type/any/index.ts b/src/type/any/index.ts new file mode 100644 index 000000000..400a3ef5c --- /dev/null +++ b/src/type/any/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './any' diff --git a/src/type/argument/argument.ts b/src/type/argument/argument.ts new file mode 100644 index 000000000..a6c63f286 --- /dev/null +++ b/src/type/argument/argument.ts @@ -0,0 +1,41 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { CreateType } from '../create/type' +import type { TSchema } from '../schema/index' +import { Kind } from '../symbols/index' + +export interface TArgument extends TSchema { + [Kind]: 'Argument' + static: unknown + index: Index +} +/** `[JavaScript]` Creates an Argument Type. */ +export function Argument(index: Index): TArgument { + return CreateType({ [Kind]: 'Argument', index }) as never +} diff --git a/src/type/argument/index.ts b/src/type/argument/index.ts new file mode 100644 index 000000000..186ff4ee6 --- /dev/null +++ b/src/type/argument/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './argument' diff --git a/src/type/array/array.ts b/src/type/array/array.ts new file mode 100644 index 000000000..bab2d7704 --- /dev/null +++ b/src/type/array/array.ts @@ -0,0 +1,59 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { CreateType } from '../create/type' +import { Ensure } from '../helpers/index' +import type { SchemaOptions, TSchema } from '../schema/index' +import type { Static } from '../static/index' +import { Kind } from '../symbols/index' + +export interface ArrayOptions extends SchemaOptions { + /** The minimum number of items in this array */ + minItems?: number + /** The maximum number of items in this array */ + maxItems?: number + /** Should this schema contain unique items */ + uniqueItems?: boolean + /** A schema for which some elements should match */ + contains?: TSchema + /** A minimum number of contains schema matches */ + minContains?: number + /** A maximum number of contains schema matches */ + maxContains?: number +} +type ArrayStatic = Ensure[]> +export interface TArray extends TSchema, ArrayOptions { + [Kind]: 'Array' + static: ArrayStatic + type: 'array' + items: T +} +/** `[Json]` Creates an Array type */ +export function Array(items: Type, options?: ArrayOptions): TArray { + return CreateType({ [Kind]: 'Array', type: 'array', items }, options) as never +} diff --git a/src/type/array/index.ts b/src/type/array/index.ts new file mode 100644 index 000000000..46e91d8a2 --- /dev/null +++ b/src/type/array/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './array' diff --git a/src/type/async-iterator/async-iterator.ts b/src/type/async-iterator/async-iterator.ts new file mode 100644 index 000000000..7a885fc77 --- /dev/null +++ b/src/type/async-iterator/async-iterator.ts @@ -0,0 +1,43 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { TSchema, SchemaOptions } from '../schema/index' +import type { Static } from '../static/index' +import { Kind } from '../symbols/index' +import { CreateType } from '../create/type' + +export interface TAsyncIterator extends TSchema { + [Kind]: 'AsyncIterator' + static: AsyncIterableIterator> + type: 'AsyncIterator' + items: T +} +/** `[JavaScript]` Creates a AsyncIterator type */ +export function AsyncIterator(items: T, options?: SchemaOptions): TAsyncIterator { + return CreateType({ [Kind]: 'AsyncIterator', type: 'AsyncIterator', items }, options) as never +} diff --git a/src/type/async-iterator/index.ts b/src/type/async-iterator/index.ts new file mode 100644 index 000000000..6a0c51edf --- /dev/null +++ b/src/type/async-iterator/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './async-iterator' diff --git a/src/type/awaited/awaited.ts b/src/type/awaited/awaited.ts new file mode 100644 index 000000000..37a84cad5 --- /dev/null +++ b/src/type/awaited/awaited.ts @@ -0,0 +1,121 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { CreateType } from '../create/type' +import { Ensure } from '../helpers/index' +import type { TSchema, SchemaOptions } from '../schema/index' +import { Computed, type TComputed } from '../computed/index' +import { Intersect, type TIntersect } from '../intersect/index' +import { Union, type TUnion } from '../union/index' +import { type TPromise } from '../promise/index' +import { Ref, type TRef } from '../ref/index' + +// ------------------------------------------------------------------ +// TypeGuard +// ------------------------------------------------------------------ +import { IsIntersect, IsUnion, IsPromise, IsRef, IsComputed } from '../guard/kind' + +// ---------------------------------------------------------------- +// FromComputed +// ---------------------------------------------------------------- +// prettier-ignore +type TFromComputed = Ensure<( + TComputed<'Awaited', [TComputed]> +)> +// prettier-ignore +function FromComputed(target: Target, parameters: Parameters): TFromComputed { + return Computed('Awaited', [Computed(target, parameters)]) as never +} +// ---------------------------------------------------------------- +// Ref +// ---------------------------------------------------------------- +type TFromRef = Ensure]>> +// prettier-ignore +function FromRef($ref: Ref): TFromRef { + return Computed('Awaited', [Ref($ref)]) as never +} +// ---------------------------------------------------------------- +// FromIntersect +// ---------------------------------------------------------------- +// prettier-ignore +type TFromIntersect = ( + TIntersect> +) +// prettier-ignore +function FromIntersect(types: [...Types]): TFromIntersect { + return Intersect(FromRest(types) as TSchema[]) as never +} +// ---------------------------------------------------------------- +// FromUnion +// ---------------------------------------------------------------- +// prettier-ignore +type TFromUnion = TUnion> +// prettier-ignore +function FromUnion(types: [...Types]): TFromUnion { + return Union(FromRest(types) as TSchema[]) as never +} +// ---------------------------------------------------------------- +// Promise +// ---------------------------------------------------------------- +type TFromPromise = TAwaited +// prettier-ignore +function FromPromise(type: Type): TFromPromise { + return Awaited(type) as never +} +// ------------------------------------------------------------------ +// FromRest +// ------------------------------------------------------------------ +// prettier-ignore +type TFromRest = ( + Types extends [infer Left extends TSchema, ...infer Right extends TSchema[]] + ? TFromRest]> + : Result +) +// prettier-ignore +function FromRest(types: [...Types]) : TFromRest { + return types.map(type => Awaited(type)) as never +} +// ------------------------------------------------------------------ +// TAwaited +// ------------------------------------------------------------------ +// prettier-ignore +export type TAwaited = ( + Type extends TComputed ? TFromComputed : + Type extends TRef ? TFromRef : + Type extends TIntersect ? TIntersect> : + Type extends TUnion ? TUnion> : + Type extends TPromise ? TAwaited : + Type +) +/** `[JavaScript]` Constructs a type by recursively unwrapping Promise types */ +export function Awaited(type: T, options?: SchemaOptions): TAwaited { + return CreateType( + IsComputed(type) ? FromComputed(type.target, type.parameters) : IsIntersect(type) ? FromIntersect(type.allOf) : IsUnion(type) ? FromUnion(type.anyOf) : IsPromise(type) ? FromPromise(type.item) : IsRef(type) ? FromRef(type.$ref) : type, + options, + ) as never +} diff --git a/src/type/awaited/index.ts b/src/type/awaited/index.ts new file mode 100644 index 000000000..6d753bb9c --- /dev/null +++ b/src/type/awaited/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './awaited' diff --git a/src/type/bigint/bigint.ts b/src/type/bigint/bigint.ts new file mode 100644 index 000000000..ad0af34c9 --- /dev/null +++ b/src/type/bigint/bigint.ts @@ -0,0 +1,48 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { TSchema, SchemaOptions } from '../schema/index' +import { Kind } from '../symbols/index' +import { CreateType } from '../create/index' + +export interface BigIntOptions extends SchemaOptions { + exclusiveMaximum?: bigint + exclusiveMinimum?: bigint + maximum?: bigint + minimum?: bigint + multipleOf?: bigint +} +export interface TBigInt extends TSchema, BigIntOptions { + [Kind]: 'BigInt' + static: bigint + type: 'bigint' +} +/** `[JavaScript]` Creates a BigInt type */ +export function BigInt(options?: BigIntOptions): TBigInt { + return CreateType({ [Kind]: 'BigInt', type: 'bigint' }, options) as never +} diff --git a/src/type/bigint/index.ts b/src/type/bigint/index.ts new file mode 100644 index 000000000..fca1fe8e8 --- /dev/null +++ b/src/type/bigint/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './bigint' diff --git a/src/type/boolean/boolean.ts b/src/type/boolean/boolean.ts new file mode 100644 index 000000000..2e582ef08 --- /dev/null +++ b/src/type/boolean/boolean.ts @@ -0,0 +1,41 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { TSchema, SchemaOptions } from '../schema/index' +import { Kind } from '../symbols/index' +import { CreateType } from '../create/index' + +export interface TBoolean extends TSchema { + [Kind]: 'Boolean' + static: boolean + type: 'boolean' +} +/** `[Json]` Creates a Boolean type */ +export function Boolean(options?: SchemaOptions): TBoolean { + return CreateType({ [Kind]: 'Boolean', type: 'boolean' }, options) as never +} diff --git a/src/type/boolean/index.ts b/src/type/boolean/index.ts new file mode 100644 index 000000000..d8620d4d7 --- /dev/null +++ b/src/type/boolean/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './boolean' diff --git a/src/type/clone/index.ts b/src/type/clone/index.ts new file mode 100644 index 000000000..c4df1782d --- /dev/null +++ b/src/type/clone/index.ts @@ -0,0 +1,30 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './type' +export * from './value' diff --git a/src/type/clone/type.ts b/src/type/clone/type.ts new file mode 100644 index 000000000..8cb09431b --- /dev/null +++ b/src/type/clone/type.ts @@ -0,0 +1,39 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { TSchema, SchemaOptions } from '../schema/index' +import { Clone } from './value' + +/** Clones a Rest */ +export function CloneRest(schemas: T): T { + return schemas.map((schema) => CloneType(schema)) as never +} +/** Clones a Type */ +export function CloneType(schema: T, options?: SchemaOptions): T { + return options === undefined ? Clone(schema) : Clone({ ...options, ...schema }) +} diff --git a/src/type/clone/value.ts b/src/type/clone/value.ts new file mode 100644 index 000000000..a20ff5f1e --- /dev/null +++ b/src/type/clone/value.ts @@ -0,0 +1,67 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import * as ValueGuard from '../guard/value' + +function ArrayType(value: unknown[]) { + return (value as any).map((value: unknown) => Visit(value as any)) +} +function DateType(value: Date) { + return new Date(value.getTime()) +} +function Uint8ArrayType(value: Uint8Array) { + return new Uint8Array(value) +} +function RegExpType(value: RegExp) { + return new RegExp(value.source, value.flags) +} +function ObjectType(value: Record) { + const result = {} as Record + for (const key of Object.getOwnPropertyNames(value)) { + result[key] = Visit(value[key]) + } + for (const key of Object.getOwnPropertySymbols(value)) { + result[key] = Visit(value[key]) + } + return result +} +// prettier-ignore +function Visit(value: unknown): any { + return ( + ValueGuard.IsArray(value) ? ArrayType(value) : + ValueGuard.IsDate(value) ? DateType(value) : + ValueGuard.IsUint8Array(value) ? Uint8ArrayType(value) : + ValueGuard.IsRegExp(value) ? RegExpType(value) : + ValueGuard.IsObject(value) ? ObjectType(value) : + value + ) +} +/** Clones a value */ +export function Clone(value: T): T { + return Visit(value) +} diff --git a/src/type/composite/composite.ts b/src/type/composite/composite.ts new file mode 100644 index 000000000..8b9833399 --- /dev/null +++ b/src/type/composite/composite.ts @@ -0,0 +1,122 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { TSchema } from '../schema/index' +import type { Evaluate } from '../helpers/index' +import { IntersectEvaluated, type TIntersectEvaluated } from '../intersect/index' +import { IndexFromPropertyKeys, type TIndexFromPropertyKeys } from '../indexed/index' +import { KeyOfPropertyKeys, type TKeyOfPropertyKeys } from '../keyof/index' +import { type TNever } from '../never/index' +import { Object, type TObject, type TProperties, type ObjectOptions } from '../object/index' +import { SetDistinct, TSetDistinct } from '../sets/index' + +// ------------------------------------------------------------------ +// TypeGuard +// ------------------------------------------------------------------ +import { IsNever } from '../guard/kind' +// ------------------------------------------------------------------ +// CompositeKeys +// ------------------------------------------------------------------ +// prettier-ignore +type TCompositeKeys = ( + T extends [infer L extends TSchema, ...infer R extends TSchema[]] + ? TCompositeKeys]> + : TSetDistinct +) +// prettier-ignore +function CompositeKeys(T: [...T]): TCompositeKeys { + const Acc = [] as PropertyKey[] + for(const L of T) Acc.push(...KeyOfPropertyKeys(L)) + return SetDistinct(Acc) as never +} +// ------------------------------------------------------------------ +// FilterNever +// ------------------------------------------------------------------ +// prettier-ignore +type TFilterNever = ( + T extends [infer L extends TSchema, ...infer R extends TSchema[]] + ? L extends TNever + ? TFilterNever + : TFilterNever + : Acc +) +// prettier-ignore +function FilterNever(T: [...T]): TFilterNever { + return T.filter(L => !IsNever(L)) as never +} +// ------------------------------------------------------------------ +// CompositeProperty +// ------------------------------------------------------------------ +// prettier-ignore +type TCompositeProperty = ( + T extends [infer L extends TSchema, ...infer R extends TSchema[]] + ? TCompositeProperty]> + : TFilterNever +) +// prettier-ignore +function CompositeProperty(T: [...T], K: K): TCompositeProperty { + const Acc = [] as TSchema[] + for(const L of T) Acc.push(...IndexFromPropertyKeys(L, [K])) + return FilterNever(Acc) as never +} +// ------------------------------------------------------------------ +// CompositeProperties +// ------------------------------------------------------------------ +// prettier-ignore +type TCompositeProperties = ( + K extends [infer L extends PropertyKey, ...infer R extends PropertyKey[]] + ? TCompositeProperties> }> + : Acc +) +// prettier-ignore +function CompositeProperties(T: [...T], K: [...K]): TCompositeProperties { + const Acc = {} as never + for(const L of K) { + Acc[L] = IntersectEvaluated(CompositeProperty(T, L)) + } + return Acc +} +// ------------------------------------------------------------------ +// Composite +// ------------------------------------------------------------------ +// prettier-ignore +type TCompositeEvaluate< + T extends TSchema[], + K extends PropertyKey[] = TCompositeKeys, + P extends TProperties = Evaluate>, + R extends TObject = TObject

+> = R +// prettier-ignore +export type TComposite = TCompositeEvaluate +// prettier-ignore +export function Composite(T: [...T], options?: ObjectOptions): TComposite { + const K = CompositeKeys(T) + const P = CompositeProperties(T, K) + const R = Object(P, options) + return R as never +} diff --git a/src/type/composite/index.ts b/src/type/composite/index.ts new file mode 100644 index 000000000..55de8e6ec --- /dev/null +++ b/src/type/composite/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './composite' diff --git a/src/type/computed/computed.ts b/src/type/computed/computed.ts new file mode 100644 index 000000000..bc8cfcb79 --- /dev/null +++ b/src/type/computed/computed.ts @@ -0,0 +1,44 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { TSchema, SchemaOptions } from '../schema/index' +import { CreateType } from '../create/index' +import { Kind } from '../symbols/symbols' + +// ------------------------------------------------------------------ +// Computed +// ------------------------------------------------------------------ +export interface TComputed extends TSchema { + [Kind]: 'Computed' + target: Target + parameters: Parameters +} +/** `[Internal]` Creates a deferred computed type. This type is used exclusively in modules to defer resolution of computable types that contain interior references */ +export function Computed(target: Target, parameters: [...Parameters], options?: SchemaOptions): TComputed { + return CreateType({ [Kind]: 'Computed', target, parameters }, options) as never +} diff --git a/src/type/computed/index.ts b/src/type/computed/index.ts new file mode 100644 index 000000000..6c75b7205 --- /dev/null +++ b/src/type/computed/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './computed' diff --git a/src/type/const/const.ts b/src/type/const/const.ts new file mode 100644 index 000000000..91e3df4f1 --- /dev/null +++ b/src/type/const/const.ts @@ -0,0 +1,135 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { AssertRest, Evaluate } from '../helpers/index' +import type { TSchema, SchemaOptions } from '../schema/index' +import type { TProperties } from '../object/index' + +import { Any, type TAny } from '../any/index' +import { BigInt, type TBigInt } from '../bigint/index' +import { Date, type TDate } from '../date/index' +import { Function as FunctionType, type TFunction } from '../function/index' +import { Literal, type TLiteral } from '../literal/index' +import { type TNever } from '../never/index' +import { Null, type TNull } from '../null/index' +import { Object, type TObject } from '../object/index' +import { Symbol, type TSymbol } from '../symbol/index' +import { Tuple, type TTuple } from '../tuple/index' +import { Readonly, type TReadonly } from '../readonly/index' +import { Undefined, type TUndefined } from '../undefined/index' +import { Uint8Array, type TUint8Array } from '../uint8array/index' +import { Unknown, type TUnknown } from '../unknown/index' +import { CreateType } from '../create/index' + +// ------------------------------------------------------------------ +// ValueGuard +// ------------------------------------------------------------------ +import { IsArray, IsNumber, IsBigInt, IsUint8Array, IsDate, IsIterator, IsObject, IsAsyncIterator, IsFunction, IsUndefined, IsNull, IsSymbol, IsBoolean, IsString } from '../guard/value' +// ------------------------------------------------------------------ +// FromArray +// ------------------------------------------------------------------ +// prettier-ignore +type TFromArray = + T extends readonly [infer L extends unknown, ...infer R extends unknown[]] + ? [FromValue, ...TFromArray] + : T +// prettier-ignore +function FromArray(T: [...T]): TFromArray { + return T.map(L => FromValue(L, false)) as never +} +// ------------------------------------------------------------------ +// FromProperties +// ------------------------------------------------------------------ +// prettier-ignore +type TFromProperties> = { + -readonly [K in keyof T]: FromValue extends infer R extends TSchema + ? TReadonly + : TReadonly +} +// prettier-ignore +function FromProperties>(value: T): TFromProperties { + const Acc = {} as TProperties + for(const K of globalThis.Object.getOwnPropertyNames(value)) Acc[K] = Readonly(FromValue(value[K], false)) + return Acc as never +} +// ------------------------------------------------------------------ +// ConditionalReadonly - Only applied if not root +// ------------------------------------------------------------------ +type TConditionalReadonly = Root extends true ? T : TReadonly +function ConditionalReadonly(T: T, root: Root): TConditionalReadonly { + return (root === true ? T : Readonly(T)) as never +} +// ------------------------------------------------------------------ +// FromValue +// ------------------------------------------------------------------ +// prettier-ignore +type FromValue = + T extends AsyncIterableIterator ? TConditionalReadonly : + T extends IterableIterator ? TConditionalReadonly : + T extends readonly unknown[] ? TReadonly>>> : + T extends Uint8Array ? TUint8Array : + T extends Date ? TDate : + T extends Record ? TConditionalReadonly>>, Root> : + T extends Function ? TConditionalReadonly, Root> : + T extends undefined ? TUndefined : + T extends null ? TNull : + T extends symbol ? TSymbol : + T extends number ? TLiteral : + T extends boolean ? TLiteral : + T extends string ? TLiteral : + T extends bigint ? TBigInt : + TObject<{}> +// prettier-ignore +function FromValue(value: T, root: Root): FromValue { + return ( + IsAsyncIterator(value) ? ConditionalReadonly(Any(), root) : + IsIterator(value) ? ConditionalReadonly(Any(), root) : + IsArray(value) ? Readonly(Tuple(FromArray(value) as TSchema[])) : + IsUint8Array(value) ? Uint8Array() : + IsDate(value) ? Date() : + IsObject(value) ? ConditionalReadonly(Object(FromProperties(value as Record) as TProperties), root) : + IsFunction(value) ? ConditionalReadonly(FunctionType([], Unknown()), root) : + IsUndefined(value) ? Undefined() : + IsNull(value) ? Null() : + IsSymbol(value) ? Symbol() : + IsBigInt(value) ? BigInt() : + IsNumber(value) ? Literal(value) : + IsBoolean(value) ? Literal(value) : + IsString(value) ? Literal(value) : + Object({}) + ) as never +} +// ------------------------------------------------------------------ +// TConst +// ------------------------------------------------------------------ +export type TConst = FromValue + +/** `[JavaScript]` Creates a readonly const type from the given value. */ +export function Const(T: T, options?: SchemaOptions): TConst { + return CreateType(FromValue(T, true), options) as never +} diff --git a/src/type/const/index.ts b/src/type/const/index.ts new file mode 100644 index 000000000..2d24ed90f --- /dev/null +++ b/src/type/const/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './const' diff --git a/src/type/constructor-parameters/constructor-parameters.ts b/src/type/constructor-parameters/constructor-parameters.ts new file mode 100644 index 000000000..3947c8539 --- /dev/null +++ b/src/type/constructor-parameters/constructor-parameters.ts @@ -0,0 +1,47 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { TSchema, SchemaOptions } from '../schema/index' +import type { TConstructor } from '../constructor/index' +import { Tuple, type TTuple } from '../tuple/index' +import { Never, type TNever } from '../never/index' +import * as KindGuard from '../guard/kind' + +// ------------------------------------------------------------------ +// ConstructorParameters +// ------------------------------------------------------------------ +// prettier-ignore +export type TConstructorParameters = ( + Type extends TConstructor + ? TTuple + : TNever +) +/** `[JavaScript]` Extracts the ConstructorParameters from the given Constructor type */ +export function ConstructorParameters(schema: Type, options?: SchemaOptions): TConstructorParameters { + return (KindGuard.IsConstructor(schema) ? Tuple(schema.parameters, options) : Never(options)) as never +} diff --git a/src/type/constructor-parameters/index.ts b/src/type/constructor-parameters/index.ts new file mode 100644 index 000000000..f5898f6b5 --- /dev/null +++ b/src/type/constructor-parameters/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './constructor-parameters' diff --git a/src/type/constructor/constructor.ts b/src/type/constructor/constructor.ts new file mode 100644 index 000000000..31539fbf0 --- /dev/null +++ b/src/type/constructor/constructor.ts @@ -0,0 +1,70 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { TSchema, SchemaOptions } from '../schema/index' +import type { Static } from '../static/index' +import type { Ensure } from '../helpers/index' +import type { TReadonlyOptional } from '../readonly-optional/index' +import type { TReadonly } from '../readonly/index' +import type { TOptional } from '../optional/index' +import { CreateType } from '../create/type' +import { Kind } from '../symbols/index' + +// ------------------------------------------------------------------ +// StaticConstructor +// ------------------------------------------------------------------ +type StaticReturnType = Static +// prettier-ignore +type StaticParameter = + T extends TReadonlyOptional ? [Readonly>?] : + T extends TReadonly ? [Readonly>] : + T extends TOptional ? [Static?] : + [Static] +// prettier-ignore +type StaticParameters = ( + T extends [infer L extends TSchema, ...infer R extends TSchema[]] + ? StaticParameters]> + : Acc +) +// prettier-ignore +type StaticConstructor = + Ensure) => StaticReturnType> +// ------------------------------------------------------------------ +// TConstructor +// ------------------------------------------------------------------ +export interface TConstructor extends TSchema { + [Kind]: 'Constructor' + static: StaticConstructor + type: 'Constructor' + parameters: T + returns: U +} +/** `[JavaScript]` Creates a Constructor type */ +export function Constructor(parameters: [...T], returns: U, options?: SchemaOptions): TConstructor { + return CreateType({ [Kind]: 'Constructor', type: 'Constructor', parameters, returns }, options) as never +} diff --git a/src/type/constructor/index.ts b/src/type/constructor/index.ts new file mode 100644 index 000000000..1f5b0684e --- /dev/null +++ b/src/type/constructor/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './constructor' diff --git a/src/type/create/immutable.ts b/src/type/create/immutable.ts new file mode 100644 index 000000000..b4041b94c --- /dev/null +++ b/src/type/create/immutable.ts @@ -0,0 +1,65 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import * as ValueGuard from '../guard/value' + +function ImmutableArray(value: unknown[]) { + return globalThis.Object.freeze(value as any).map((value: unknown) => Immutable(value as any)) +} +function ImmutableDate(value: Date) { + return value +} +function ImmutableUint8Array(value: Uint8Array) { + return value +} +function ImmutableRegExp(value: RegExp) { + return value +} +function ImmutableObject(value: Record) { + const result = {} as Record + for (const key of Object.getOwnPropertyNames(value)) { + result[key] = Immutable(value[key]) + } + for (const key of Object.getOwnPropertySymbols(value)) { + result[key] = Immutable(value[key]) + } + return globalThis.Object.freeze(result) +} + +/** Specialized deep immutable value. Applies freeze recursively to the given value */ +// prettier-ignore +export function Immutable(value: unknown): unknown { + return ( + ValueGuard.IsArray(value) ? ImmutableArray(value) : + ValueGuard.IsDate(value) ? ImmutableDate(value) : + ValueGuard.IsUint8Array(value) ? ImmutableUint8Array(value) : + ValueGuard.IsRegExp(value) ? ImmutableRegExp(value) : + ValueGuard.IsObject(value) ? ImmutableObject(value) : + value + ) +} diff --git a/src/type/create/index.ts b/src/type/create/index.ts new file mode 100644 index 000000000..ea69721de --- /dev/null +++ b/src/type/create/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './type' diff --git a/src/type/create/type.ts b/src/type/create/type.ts new file mode 100644 index 000000000..253231d35 --- /dev/null +++ b/src/type/create/type.ts @@ -0,0 +1,45 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { TypeSystemPolicy } from '../../system/policy' +import { SchemaOptions } from '../schema/schema' +import { Immutable } from './immutable' +import { Clone } from '../clone/value' + +/** Creates TypeBox schematics using the configured InstanceMode */ +export function CreateType(schema: Record, options?: SchemaOptions): unknown { + const result = options !== undefined ? { ...options, ...schema } : schema + switch (TypeSystemPolicy.InstanceMode) { + case 'freeze': + return Immutable(result) + case 'clone': + return Clone(result) + default: + return result + } +} diff --git a/src/type/date/date.ts b/src/type/date/date.ts new file mode 100644 index 000000000..040e64e8e --- /dev/null +++ b/src/type/date/date.ts @@ -0,0 +1,53 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { TSchema, SchemaOptions } from '../schema/index' +import { Kind } from '../symbols/index' +import { CreateType } from '../create/type' + +export interface DateOptions extends SchemaOptions { + /** The exclusive maximum timestamp value */ + exclusiveMaximumTimestamp?: number + /** The exclusive minimum timestamp value */ + exclusiveMinimumTimestamp?: number + /** The maximum timestamp value */ + maximumTimestamp?: number + /** The minimum timestamp value */ + minimumTimestamp?: number + /** The multiple of timestamp value */ + multipleOfTimestamp?: number +} +export interface TDate extends TSchema, DateOptions { + [Kind]: 'Date' + static: Date + type: 'date' +} +/** `[JavaScript]` Creates a Date type */ +export function Date(options?: DateOptions): TDate { + return CreateType({ [Kind]: 'Date', type: 'Date' }, options) as never +} diff --git a/src/type/date/index.ts b/src/type/date/index.ts new file mode 100644 index 000000000..6aff28867 --- /dev/null +++ b/src/type/date/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './date' diff --git a/src/type/discard/discard.ts b/src/type/discard/discard.ts new file mode 100644 index 000000000..821f9b40c --- /dev/null +++ b/src/type/discard/discard.ts @@ -0,0 +1,36 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +function DiscardKey(value: Record, key: PropertyKey) { + const { [key]: _, ...rest } = value + return rest +} +/** Discards property keys from the given value. This function returns a shallow Clone. */ +export function Discard(value: Record, keys: PropertyKey[]) { + return keys.reduce((acc, key) => DiscardKey(acc, key), value) +} diff --git a/src/type/discard/index.ts b/src/type/discard/index.ts new file mode 100644 index 000000000..0e9830749 --- /dev/null +++ b/src/type/discard/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './discard' diff --git a/src/type/enum/enum.ts b/src/type/enum/enum.ts new file mode 100644 index 000000000..87f88e22c --- /dev/null +++ b/src/type/enum/enum.ts @@ -0,0 +1,58 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { TSchema, SchemaOptions } from '../schema/index' +import { Literal, type TLiteral } from '../literal/index' +import { Kind, Hint } from '../symbols/index' +import { Union } from '../union/index' +// ------------------------------------------------------------------ +// ValueGuard +// ------------------------------------------------------------------ +import { IsUndefined } from '../guard/value' +// ------------------------------------------------------------------ +// TEnum +// ------------------------------------------------------------------ +export type TEnumRecord = Record +export type TEnumValue = string | number +export type TEnumKey = string +export interface TEnum = Record> extends TSchema { + [Kind]: 'Union' + [Hint]: 'Enum' + static: T[keyof T] + anyOf: TLiteral[] +} +/** `[Json]` Creates a Enum type */ +export function Enum>(item: T, options?: SchemaOptions): TEnum { + if (IsUndefined(item)) throw new Error('Enum undefined or empty') + const values1 = globalThis.Object.getOwnPropertyNames(item) + .filter((key) => isNaN(key as any)) + .map((key) => item[key]) as T[keyof T][] + const values2 = [...new Set(values1)] + const anyOf = values2.map((value) => Literal(value)) + return Union(anyOf, { ...options, [Hint]: 'Enum' }) as never +} diff --git a/src/type/enum/index.ts b/src/type/enum/index.ts new file mode 100644 index 000000000..8ad69431d --- /dev/null +++ b/src/type/enum/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './enum' diff --git a/src/type/error/error.ts b/src/type/error/error.ts new file mode 100644 index 000000000..443a56338 --- /dev/null +++ b/src/type/error/error.ts @@ -0,0 +1,34 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +/** The base Error type thrown for all TypeBox exceptions */ +export class TypeBoxError extends Error { + constructor(message: string) { + super(message) + } +} diff --git a/src/type/error/index.ts b/src/type/error/index.ts new file mode 100644 index 000000000..2e03c77bd --- /dev/null +++ b/src/type/error/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './error' diff --git a/src/type/exclude/exclude-from-mapped-result.ts b/src/type/exclude/exclude-from-mapped-result.ts new file mode 100644 index 000000000..ab8fef55a --- /dev/null +++ b/src/type/exclude/exclude-from-mapped-result.ts @@ -0,0 +1,89 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { TSchema } from '../schema/index' +import type { TProperties } from '../object/index' +import { MappedResult, type TMappedResult } from '../mapped/index' +import { Exclude, type TExclude } from './exclude' + +// ------------------------------------------------------------------ +// FromProperties +// ------------------------------------------------------------------ +// prettier-ignore +type TFromProperties< + K extends TProperties, + T extends TSchema +> = ( + { [K2 in keyof K]: TExclude } +) +// prettier-ignore +function FromProperties< + P extends TProperties, + T extends TSchema +>(P: P, U: T): TFromProperties { + const Acc = {} as TProperties + for(const K2 of globalThis.Object.getOwnPropertyNames(P)) Acc[K2] = Exclude(P[K2], U) + return Acc as never +} +// ------------------------------------------------------------------ +// FromMappedResult +// ------------------------------------------------------------------ +// prettier-ignore +type TFromMappedResult< + R extends TMappedResult, + T extends TSchema +> = ( + TFromProperties + ) +// prettier-ignore +function FromMappedResult< + R extends TMappedResult, + T extends TSchema +>(R: R, T: T): TFromMappedResult { + return FromProperties(R.properties, T) as never +} +// ------------------------------------------------------------------ +// ExcludeFromMappedResult +// ------------------------------------------------------------------ +// prettier-ignore +export type TExcludeFromMappedResult< + R extends TMappedResult, + T extends TSchema, + P extends TProperties = TFromMappedResult +> = ( + TMappedResult

+) +// prettier-ignore +export function ExcludeFromMappedResult< + R extends TMappedResult, + T extends TSchema, + P extends TProperties = TFromMappedResult +>(R: R, T: T): TMappedResult

{ + const P = FromMappedResult(R, T) + return MappedResult(P) as never +} diff --git a/src/type/exclude/exclude-from-template-literal.ts b/src/type/exclude/exclude-from-template-literal.ts new file mode 100644 index 000000000..a80f10a2f --- /dev/null +++ b/src/type/exclude/exclude-from-template-literal.ts @@ -0,0 +1,39 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { TSchema } from '../schema/index' +import { TExclude, Exclude } from './exclude' +import { TemplateLiteralToUnion, type TTemplateLiteral, type TTemplateLiteralToUnion } from '../template-literal/index' + +// prettier-ignore +export type TExcludeFromTemplateLiteral = ( + TExclude, R> +) +export function ExcludeFromTemplateLiteral(L: L, R: R): TExcludeFromTemplateLiteral { + return Exclude(TemplateLiteralToUnion(L), R) as never +} diff --git a/src/type/exclude/exclude.ts b/src/type/exclude/exclude.ts new file mode 100644 index 000000000..663cf73bc --- /dev/null +++ b/src/type/exclude/exclude.ts @@ -0,0 +1,82 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { CreateType } from '../create/type' +import type { TSchema, SchemaOptions } from '../schema/index' +import type { UnionToTuple, AssertRest, AssertType } from '../helpers/index' +import type { TMappedResult } from '../mapped/index' +import { type TTemplateLiteral } from '../template-literal/index' +import { Union, type TUnion } from '../union/index' +import { Never, type TNever } from '../never/index' +import { type Static } from '../static/index' +import { type TUnionEvaluated } from '../union/index' +import { ExtendsCheck, ExtendsResult } from '../extends/index' +import { ExcludeFromMappedResult, type TExcludeFromMappedResult } from './exclude-from-mapped-result' +import { ExcludeFromTemplateLiteral, type TExcludeFromTemplateLiteral } from './exclude-from-template-literal' + +// ------------------------------------------------------------------ +// TypeGuard +// ------------------------------------------------------------------ +import { IsMappedResult, IsTemplateLiteral, IsUnion } from '../guard/kind' +// ------------------------------------------------------------------ +// ExcludeRest +// ------------------------------------------------------------------ +// prettier-ignore +type TExcludeRest = AssertRest> extends Static ? never : L[K] +}[number]>> extends infer R extends TSchema[] ? TUnionEvaluated : never + +function ExcludeRest(L: [...L], R: R) { + const excluded = L.filter((inner) => ExtendsCheck(inner, R) === ExtendsResult.False) + return excluded.length === 1 ? excluded[0] : Union(excluded) +} +// ------------------------------------------------------------------ +// TExclude +// ------------------------------------------------------------------ +// prettier-ignore +export type TExclude = ( + L extends TUnion ? TExcludeRest : + L extends R ? TNever : L +) +/** `[Json]` Constructs a type by excluding from unionType all union members that are assignable to excludedMembers */ +export function Exclude(unionType: L, excludedMembers: R, options?: SchemaOptions): TExcludeFromMappedResult +/** `[Json]` Constructs a type by excluding from unionType all union members that are assignable to excludedMembers */ +export function Exclude(unionType: L, excludedMembers: R, options?: SchemaOptions): TExcludeFromTemplateLiteral +/** `[Json]` Constructs a type by excluding from unionType all union members that are assignable to excludedMembers */ +export function Exclude(unionType: L, excludedMembers: R, options?: SchemaOptions): TExclude +/** `[Json]` Constructs a type by excluding from unionType all union members that are assignable to excludedMembers */ +export function Exclude(L: TSchema, R: TSchema, options: SchemaOptions = {}): any { + // overloads + if (IsTemplateLiteral(L)) return CreateType(ExcludeFromTemplateLiteral(L, R), options) + if (IsMappedResult(L)) return CreateType(ExcludeFromMappedResult(L, R), options) + // prettier-ignore + return CreateType( + IsUnion(L) ? ExcludeRest(L.anyOf, R) : + ExtendsCheck(L, R) !== ExtendsResult.False ? Never() : L + , options) +} diff --git a/src/type/exclude/index.ts b/src/type/exclude/index.ts new file mode 100644 index 000000000..5e725fb54 --- /dev/null +++ b/src/type/exclude/index.ts @@ -0,0 +1,31 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './exclude-from-mapped-result' +export * from './exclude-from-template-literal' +export * from './exclude' diff --git a/src/type/extends/extends-check.ts b/src/type/extends/extends-check.ts new file mode 100644 index 000000000..e78bbc318 --- /dev/null +++ b/src/type/extends/extends-check.ts @@ -0,0 +1,776 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { type TAny, Any } from '../any/index' +import { type TArray } from '../array/index' +import { type TAsyncIterator } from '../async-iterator/index' +import { type TBigInt } from '../bigint/index' +import { type TBoolean } from '../boolean/index' +import { type TConstructor } from '../constructor/index' +import { type TDate } from '../date/index' +import { type TFunction, Function as FunctionType } from '../function/index' +import { type TInteger } from '../integer/index' +import { type TIntersect } from '../intersect/index' +import { type TIterator } from '../iterator/index' +import { type TLiteral } from '../literal/index' +import { type TNever } from '../never/index' +import { type TNot } from '../not/index' +import { type TNull } from '../null/index' +import { type TNumber, Number } from '../number/index' +import { type TObject } from '../object/index' +import { type TPromise } from '../promise/index' +import { type TRecord } from '../record/index' +import { type TSchema } from '../schema/index' +import { type TString, String } from '../string/index' +import { type TSymbol } from '../symbol/index' +import { type TTuple } from '../tuple/index' +import { type TUint8Array } from '../uint8array/index' +import { type TUndefined } from '../undefined/index' +import { type TUnion } from '../union/index' +import { type TUnknown, Unknown } from '../unknown/index' +import { type TVoid } from '../void/index' + +import { TemplateLiteralToUnion } from '../template-literal/index' +import { PatternNumberExact, PatternStringExact } from '../patterns/index' +import { Kind, Hint } from '../symbols/index' +import { TypeBoxError } from '../error/index' +import { TypeGuard, ValueGuard } from '../guard/index' + +export class ExtendsResolverError extends TypeBoxError {} + +export enum ExtendsResult { + Union, + True, + False, +} +// ------------------------------------------------------------------ +// IntoBooleanResult +// ------------------------------------------------------------------ +// prettier-ignore +function IntoBooleanResult(result: ExtendsResult) { + return result === ExtendsResult.False ? result : ExtendsResult.True +} +// ------------------------------------------------------------------ +// Throw +// ------------------------------------------------------------------ +// prettier-ignore +function Throw(message: string): never { + throw new ExtendsResolverError(message) +} +// ------------------------------------------------------------------ +// StructuralRight +// ------------------------------------------------------------------ +// prettier-ignore +function IsStructuralRight(right: TSchema): boolean { + return ( + TypeGuard.IsNever(right) || + TypeGuard.IsIntersect(right) || + TypeGuard.IsUnion(right) || + TypeGuard.IsUnknown(right) || + TypeGuard.IsAny(right) + ) +} +// prettier-ignore +function StructuralRight(left: TSchema, right: TSchema) { + return ( + TypeGuard.IsNever(right) ? FromNeverRight(left, right) : + TypeGuard.IsIntersect(right) ? FromIntersectRight(left, right) : + TypeGuard.IsUnion(right) ? FromUnionRight(left, right) : + TypeGuard.IsUnknown(right) ? FromUnknownRight(left, right) : + TypeGuard.IsAny(right) ? FromAnyRight(left, right) : + Throw('StructuralRight') + ) +} +// ------------------------------------------------------------------ +// Any +// ------------------------------------------------------------------ +// prettier-ignore +function FromAnyRight(left: TSchema, right: TAny) { + return ExtendsResult.True +} +// prettier-ignore +function FromAny(left: TAny, right: TSchema) { + return ( + TypeGuard.IsIntersect(right) ? FromIntersectRight(left, right) : + (TypeGuard.IsUnion(right) && right.anyOf.some((schema) => TypeGuard.IsAny(schema) || TypeGuard.IsUnknown(schema))) ? ExtendsResult.True : + TypeGuard.IsUnion(right) ? ExtendsResult.Union : + TypeGuard.IsUnknown(right) ? ExtendsResult.True : + TypeGuard.IsAny(right) ? ExtendsResult.True : + ExtendsResult.Union + ) +} +// ------------------------------------------------------------------ +// Array +// ------------------------------------------------------------------ +// prettier-ignore +function FromArrayRight(left: TSchema, right: TArray) { + return ( + TypeGuard.IsUnknown(left) ? ExtendsResult.False : + TypeGuard.IsAny(left) ? ExtendsResult.Union : + TypeGuard.IsNever(left) ? ExtendsResult.True : + ExtendsResult.False + ) +} +// prettier-ignore +function FromArray(left: TArray, right: TSchema) { + return ( + TypeGuard.IsObject(right) && IsObjectArrayLike(right) ? ExtendsResult.True : + IsStructuralRight(right) ? StructuralRight(left, right) : + !TypeGuard.IsArray(right) ? ExtendsResult.False : + IntoBooleanResult(Visit(left.items, right.items)) + ) +} +// ------------------------------------------------------------------ +// AsyncIterator +// ------------------------------------------------------------------ +// prettier-ignore +function FromAsyncIterator(left: TAsyncIterator, right: TSchema) { + return ( + IsStructuralRight(right) ? StructuralRight(left, right) : + !TypeGuard.IsAsyncIterator(right) ? ExtendsResult.False : + IntoBooleanResult(Visit(left.items, right.items)) + ) +} +// ------------------------------------------------------------------ +// BigInt +// ------------------------------------------------------------------ +// prettier-ignore +function FromBigInt(left: TBigInt, right: TSchema): ExtendsResult { + return ( + IsStructuralRight(right) ? StructuralRight(left, right) : + TypeGuard.IsObject(right) ? FromObjectRight(left, right) : + TypeGuard.IsRecord(right) ? FromRecordRight(left, right) : + TypeGuard.IsBigInt(right) ? ExtendsResult.True : + ExtendsResult.False + ) +} +// ------------------------------------------------------------------ +// Boolean +// ------------------------------------------------------------------ +// prettier-ignore +function FromBooleanRight(left: TSchema, right: TBoolean) { + return ( + TypeGuard.IsLiteralBoolean(left) ? ExtendsResult.True : + TypeGuard.IsBoolean(left) ? ExtendsResult.True : + ExtendsResult.False + ) +} +// prettier-ignore +function FromBoolean(left: TBoolean, right: TSchema): ExtendsResult { + return ( + IsStructuralRight(right) ? StructuralRight(left, right) : + TypeGuard.IsObject(right) ? FromObjectRight(left, right) : + TypeGuard.IsRecord(right) ? FromRecordRight(left, right) : + TypeGuard.IsBoolean(right) ? ExtendsResult.True : + ExtendsResult.False + ) +} +// ------------------------------------------------------------------ +// Constructor +// ------------------------------------------------------------------ +// prettier-ignore +function FromConstructor(left: TConstructor, right: TSchema) { + return ( + IsStructuralRight(right) ? StructuralRight(left, right) : + TypeGuard.IsObject(right) ? FromObjectRight(left, right) : + !TypeGuard.IsConstructor(right) ? ExtendsResult.False : + left.parameters.length > right.parameters.length ? ExtendsResult.False : + (!left.parameters.every((schema, index) => IntoBooleanResult(Visit(right.parameters[index], schema)) === ExtendsResult.True)) ? ExtendsResult.False : + IntoBooleanResult(Visit(left.returns, right.returns)) + ) +} +// ------------------------------------------------------------------ +// Date +// ------------------------------------------------------------------ +// prettier-ignore +function FromDate(left: TDate, right: TSchema) { + return ( + IsStructuralRight(right) ? StructuralRight(left, right) : + TypeGuard.IsObject(right) ? FromObjectRight(left, right) : + TypeGuard.IsRecord(right) ? FromRecordRight(left, right) : + TypeGuard.IsDate(right) ? ExtendsResult.True : + ExtendsResult.False + ) +} +// ------------------------------------------------------------------ +// Function +// ------------------------------------------------------------------ +// prettier-ignore +function FromFunction(left: TFunction, right: TSchema) { + return ( + IsStructuralRight(right) ? StructuralRight(left, right) : + TypeGuard.IsObject(right) ? FromObjectRight(left, right) : + !TypeGuard.IsFunction(right) ? ExtendsResult.False : + left.parameters.length > right.parameters.length ? ExtendsResult.False : + (!left.parameters.every((schema, index) => IntoBooleanResult(Visit(right.parameters[index], schema)) === ExtendsResult.True)) ? ExtendsResult.False : + IntoBooleanResult(Visit(left.returns, right.returns)) + ) +} +// ------------------------------------------------------------------ +// Integer +// ------------------------------------------------------------------ +// prettier-ignore +function FromIntegerRight(left: TSchema, right: TInteger) { + return ( + TypeGuard.IsLiteral(left) && ValueGuard.IsNumber(left.const) ? ExtendsResult.True : + TypeGuard.IsNumber(left) || TypeGuard.IsInteger(left) ? ExtendsResult.True : + ExtendsResult.False + ) +} +// prettier-ignore +function FromInteger(left: TInteger, right: TSchema): ExtendsResult { + return ( + TypeGuard.IsInteger(right) || TypeGuard.IsNumber(right) ? ExtendsResult.True : + IsStructuralRight(right) ? StructuralRight(left, right) : + TypeGuard.IsObject(right) ? FromObjectRight(left, right) : + TypeGuard.IsRecord(right) ? FromRecordRight(left, right) : + ExtendsResult.False + ) +} +// ------------------------------------------------------------------ +// Intersect +// ------------------------------------------------------------------ +// prettier-ignore +function FromIntersectRight(left: TSchema, right: TIntersect): ExtendsResult { + return right.allOf.every((schema) => Visit(left, schema) === ExtendsResult.True) + ? ExtendsResult.True + : ExtendsResult.False +} +// prettier-ignore +function FromIntersect(left: TIntersect, right: TSchema) { + return left.allOf.some((schema) => Visit(schema, right) === ExtendsResult.True) + ? ExtendsResult.True + : ExtendsResult.False +} +// ------------------------------------------------------------------ +// Iterator +// ------------------------------------------------------------------ +// prettier-ignore +function FromIterator(left: TIterator, right: TSchema) { + return ( + IsStructuralRight(right) ? StructuralRight(left, right) : + !TypeGuard.IsIterator(right) ? ExtendsResult.False : + IntoBooleanResult(Visit(left.items, right.items)) + ) +} +// ------------------------------------------------------------------ +// Literal +// ------------------------------------------------------------------ +// prettier-ignore +function FromLiteral(left: TLiteral, right: TSchema): ExtendsResult { + return ( + TypeGuard.IsLiteral(right) && right.const === left.const ? ExtendsResult.True : + IsStructuralRight(right) ? StructuralRight(left, right) : + TypeGuard.IsObject(right) ? FromObjectRight(left, right) : + TypeGuard.IsRecord(right) ? FromRecordRight(left, right) : + TypeGuard.IsString(right) ? FromStringRight(left, right) : + TypeGuard.IsNumber(right) ? FromNumberRight(left, right) : + TypeGuard.IsInteger(right) ? FromIntegerRight(left, right) : + TypeGuard.IsBoolean(right) ? FromBooleanRight(left, right) : + ExtendsResult.False + ) +} +// ------------------------------------------------------------------ +// Never +// ------------------------------------------------------------------ +// prettier-ignore +function FromNeverRight(left: TSchema, right: TNever) { + return ExtendsResult.False +} +// prettier-ignore +function FromNever(left: TNever, right: TSchema) { + return ExtendsResult.True +} +// ------------------------------------------------------------------ +// Not +// ------------------------------------------------------------------ +// prettier-ignore +function UnwrapTNot(schema: T): TUnknown | TNot['not'] { + let [current, depth]: [TSchema, number] = [schema, 0] + while (true) { + if (!TypeGuard.IsNot(current)) break + current = current.not + depth += 1 + } + return depth % 2 === 0 ? current : Unknown() +} +// prettier-ignore +function FromNot(left: TSchema, right: TSchema) { + // TypeScript has no concept of negated types, and attempts to correctly check the negated + // type at runtime would put TypeBox at odds with TypeScripts ability to statically infer + // the type. Instead we unwrap to either unknown or T and continue evaluating. + // prettier-ignore + return ( + TypeGuard.IsNot(left) ? Visit(UnwrapTNot(left), right) : + TypeGuard.IsNot(right) ? Visit(left, UnwrapTNot(right)) : + Throw('Invalid fallthrough for Not') + ) +} +// ------------------------------------------------------------------ +// Null +// ------------------------------------------------------------------ +// prettier-ignore +function FromNull(left: TNull, right: TSchema) { + return ( + IsStructuralRight(right) ? StructuralRight(left, right) : + TypeGuard.IsObject(right) ? FromObjectRight(left, right) : + TypeGuard.IsRecord(right) ? FromRecordRight(left, right) : + TypeGuard.IsNull(right) ? ExtendsResult.True : + ExtendsResult.False + ) +} +// ------------------------------------------------------------------ +// Number +// ------------------------------------------------------------------ +// prettier-ignore +function FromNumberRight(left: TSchema, right: TNumber) { + return ( + TypeGuard.IsLiteralNumber(left) ? ExtendsResult.True : + TypeGuard.IsNumber(left) || TypeGuard.IsInteger(left) ? ExtendsResult.True : + ExtendsResult.False + ) +} +// prettier-ignore +function FromNumber(left: TNumber, right: TSchema): ExtendsResult { + return ( + IsStructuralRight(right) ? StructuralRight(left, right) : + TypeGuard.IsObject(right) ? FromObjectRight(left, right) : + TypeGuard.IsRecord(right) ? FromRecordRight(left, right) : + TypeGuard.IsInteger(right) || TypeGuard.IsNumber(right) ? ExtendsResult.True : + ExtendsResult.False + ) +} +// ------------------------------------------------------------------ +// Object +// ------------------------------------------------------------------ +// prettier-ignore +function IsObjectPropertyCount(schema: TObject, count: number) { + return Object.getOwnPropertyNames(schema.properties).length === count +} +// prettier-ignore +function IsObjectStringLike(schema: TObject) { + return IsObjectArrayLike(schema) +} +// prettier-ignore +function IsObjectSymbolLike(schema: TObject) { + return IsObjectPropertyCount(schema, 0) || ( + IsObjectPropertyCount(schema, 1) && 'description' in schema.properties && TypeGuard.IsUnion(schema.properties.description) && schema.properties.description.anyOf.length === 2 && (( + TypeGuard.IsString(schema.properties.description.anyOf[0]) && + TypeGuard.IsUndefined(schema.properties.description.anyOf[1]) + ) || ( + TypeGuard.IsString(schema.properties.description.anyOf[1]) && + TypeGuard.IsUndefined(schema.properties.description.anyOf[0]) + )) + ) +} +// prettier-ignore +function IsObjectNumberLike(schema: TObject) { + return IsObjectPropertyCount(schema, 0) +} +// prettier-ignore +function IsObjectBooleanLike(schema: TObject) { + return IsObjectPropertyCount(schema, 0) +} +// prettier-ignore +function IsObjectBigIntLike(schema: TObject) { + return IsObjectPropertyCount(schema, 0) +} +// prettier-ignore +function IsObjectDateLike(schema: TObject) { + return IsObjectPropertyCount(schema, 0) +} +// prettier-ignore +function IsObjectUint8ArrayLike(schema: TObject) { + return IsObjectArrayLike(schema) +} +// prettier-ignore +function IsObjectFunctionLike(schema: TObject) { + const length = Number() + return IsObjectPropertyCount(schema, 0) || (IsObjectPropertyCount(schema, 1) && 'length' in schema.properties && IntoBooleanResult(Visit(schema.properties['length'], length)) === ExtendsResult.True) +} +// prettier-ignore +function IsObjectConstructorLike(schema: TObject) { + return IsObjectPropertyCount(schema, 0) +} +// prettier-ignore +function IsObjectArrayLike(schema: TObject) { + const length = Number() + return IsObjectPropertyCount(schema, 0) || (IsObjectPropertyCount(schema, 1) && 'length' in schema.properties && IntoBooleanResult(Visit(schema.properties['length'], length)) === ExtendsResult.True) +} +// prettier-ignore +function IsObjectPromiseLike(schema: TObject) { + const then = FunctionType([Any()], Any()) + return IsObjectPropertyCount(schema, 0) || (IsObjectPropertyCount(schema, 1) && 'then' in schema.properties && IntoBooleanResult(Visit(schema.properties['then'], then)) === ExtendsResult.True) +} +// ------------------------------------------------------------------ +// Property +// ------------------------------------------------------------------ +// prettier-ignore +function Property(left: TSchema, right: TSchema) { + return ( + Visit(left, right) === ExtendsResult.False ? ExtendsResult.False : + TypeGuard.IsOptional(left) && !TypeGuard.IsOptional(right) ? ExtendsResult.False : + ExtendsResult.True + ) +} +// prettier-ignore +function FromObjectRight(left: TSchema, right: TObject) { + return ( + TypeGuard.IsUnknown(left) ? ExtendsResult.False : + TypeGuard.IsAny(left) ? ExtendsResult.Union : ( + TypeGuard.IsNever(left) || + (TypeGuard.IsLiteralString(left) && IsObjectStringLike(right)) || + (TypeGuard.IsLiteralNumber(left) && IsObjectNumberLike(right)) || + (TypeGuard.IsLiteralBoolean(left) && IsObjectBooleanLike(right)) || + (TypeGuard.IsSymbol(left) && IsObjectSymbolLike(right)) || + (TypeGuard.IsBigInt(left) && IsObjectBigIntLike(right)) || + (TypeGuard.IsString(left) && IsObjectStringLike(right)) || + (TypeGuard.IsSymbol(left) && IsObjectSymbolLike(right)) || + (TypeGuard.IsNumber(left) && IsObjectNumberLike(right)) || + (TypeGuard.IsInteger(left) && IsObjectNumberLike(right)) || + (TypeGuard.IsBoolean(left) && IsObjectBooleanLike(right)) || + (TypeGuard.IsUint8Array(left) && IsObjectUint8ArrayLike(right)) || + (TypeGuard.IsDate(left) && IsObjectDateLike(right)) || + (TypeGuard.IsConstructor(left) && IsObjectConstructorLike(right)) || + (TypeGuard.IsFunction(left) && IsObjectFunctionLike(right)) + ) ? ExtendsResult.True : + (TypeGuard.IsRecord(left) && TypeGuard.IsString(RecordKey(left))) ? (() => { + // When expressing a Record with literal key values, the Record is converted into a Object with + // the Hint assigned as `Record`. This is used to invert the extends logic. + return right[Hint] === 'Record' ? ExtendsResult.True : ExtendsResult.False + })() : + (TypeGuard.IsRecord(left) && TypeGuard.IsNumber(RecordKey(left))) ? (() => { + return IsObjectPropertyCount(right, 0) ? ExtendsResult.True : ExtendsResult.False + })() : + ExtendsResult.False + ) +} +// prettier-ignore +function FromObject(left: TObject, right: TSchema) { + return ( + IsStructuralRight(right) ? StructuralRight(left, right) : + TypeGuard.IsRecord(right) ? FromRecordRight(left, right) : + !TypeGuard.IsObject(right) ? ExtendsResult.False : + (() => { + for (const key of Object.getOwnPropertyNames(right.properties)) { + if (!(key in left.properties) && !TypeGuard.IsOptional(right.properties[key])) { + return ExtendsResult.False + } + if (TypeGuard.IsOptional(right.properties[key])) { + return ExtendsResult.True + } + if (Property(left.properties[key], right.properties[key]) === ExtendsResult.False) { + return ExtendsResult.False + } + } + return ExtendsResult.True + })() + ) +} +// ------------------------------------------------------------------ +// Promise +// ------------------------------------------------------------------ +// prettier-ignore +function FromPromise(left: TPromise, right: TSchema) { + return ( + IsStructuralRight(right) ? StructuralRight(left, right) : + TypeGuard.IsObject(right) && IsObjectPromiseLike(right) ? ExtendsResult.True : + !TypeGuard.IsPromise(right) ? ExtendsResult.False : + IntoBooleanResult(Visit(left.item, right.item)) + ) +} +// ------------------------------------------------------------------ +// Record +// ------------------------------------------------------------------ +// prettier-ignore +function RecordKey(schema: TRecord) { + return ( + PatternNumberExact in schema.patternProperties ? Number() : + PatternStringExact in schema.patternProperties ? String() : + Throw('Unknown record key pattern') + ) +} +// prettier-ignore +function RecordValue(schema: TRecord) { + return ( + PatternNumberExact in schema.patternProperties ? schema.patternProperties[PatternNumberExact] : + PatternStringExact in schema.patternProperties ? schema.patternProperties[PatternStringExact] : + Throw('Unable to get record value schema') + ) +} +// prettier-ignore +function FromRecordRight(left: TSchema, right: TRecord) { + const [Key, Value] = [RecordKey(right), RecordValue(right)] + return ( + ( + TypeGuard.IsLiteralString(left) && TypeGuard.IsNumber(Key) && IntoBooleanResult(Visit(left, Value)) === ExtendsResult.True) ? ExtendsResult.True : + TypeGuard.IsUint8Array(left) && TypeGuard.IsNumber(Key) ? Visit(left, Value) : + TypeGuard.IsString(left) && TypeGuard.IsNumber(Key) ? Visit(left, Value) : + TypeGuard.IsArray(left) && TypeGuard.IsNumber(Key) ? Visit(left, Value) : + TypeGuard.IsObject(left) ? (() => { + for (const key of Object.getOwnPropertyNames(left.properties)) { + if (Property(Value, left.properties[key]) === ExtendsResult.False) { + return ExtendsResult.False + } + } + return ExtendsResult.True + })() : + ExtendsResult.False + ) +} +// prettier-ignore +function FromRecord(left: TRecord, right: TSchema) { + return ( + IsStructuralRight(right) ? StructuralRight(left, right) : + TypeGuard.IsObject(right) ? FromObjectRight(left, right) : + !TypeGuard.IsRecord(right) ? ExtendsResult.False : + Visit(RecordValue(left), RecordValue(right)) + ) +} +// ------------------------------------------------------------------ +// RegExp +// ------------------------------------------------------------------ +// prettier-ignore +function FromRegExp(left: TSchema, right: TSchema) { + // Note: RegExp types evaluate as strings, not RegExp objects. + // Here we remap either into string and continue evaluating. + const L = TypeGuard.IsRegExp(left) ? String() : left + const R = TypeGuard.IsRegExp(right) ? String() : right + return Visit(L, R) +} +// ------------------------------------------------------------------ +// String +// ------------------------------------------------------------------ +// prettier-ignore +function FromStringRight(left: TSchema, right: TString) { + return ( + TypeGuard.IsLiteral(left) && ValueGuard.IsString(left.const) ? ExtendsResult.True : + TypeGuard.IsString(left) ? ExtendsResult.True : + ExtendsResult.False + ) +} +// prettier-ignore +function FromString(left: TString, right: TSchema): ExtendsResult { + return ( + IsStructuralRight(right) ? StructuralRight(left, right) : + TypeGuard.IsObject(right) ? FromObjectRight(left, right) : + TypeGuard.IsRecord(right) ? FromRecordRight(left, right) : + TypeGuard.IsString(right) ? ExtendsResult.True : + ExtendsResult.False + ) +} +// ------------------------------------------------------------------ +// Symbol +// ------------------------------------------------------------------ +// prettier-ignore +function FromSymbol(left: TSymbol, right: TSchema): ExtendsResult { + return ( + IsStructuralRight(right) ? StructuralRight(left, right) : + TypeGuard.IsObject(right) ? FromObjectRight(left, right) : + TypeGuard.IsRecord(right) ? FromRecordRight(left, right) : + TypeGuard.IsSymbol(right) ? ExtendsResult.True : + ExtendsResult.False + ) +} +// ------------------------------------------------------------------ +// TemplateLiteral +// ------------------------------------------------------------------ +// prettier-ignore +function FromTemplateLiteral(left: TSchema, right: TSchema) { + // TemplateLiteral types are resolved to either unions for finite expressions or string + // for infinite expressions. Here we call to TemplateLiteralResolver to resolve for + // either type and continue evaluating. + return ( + TypeGuard.IsTemplateLiteral(left) ? Visit(TemplateLiteralToUnion(left), right) : + TypeGuard.IsTemplateLiteral(right) ? Visit(left, TemplateLiteralToUnion(right)) : + Throw('Invalid fallthrough for TemplateLiteral') + ) +} +// ------------------------------------------------------------------ +// Tuple +// ------------------------------------------------------------------ +// prettier-ignore +function IsArrayOfTuple(left: TTuple, right: TSchema) { + return ( + TypeGuard.IsArray(right) && + left.items !== undefined && + left.items.every((schema) => Visit(schema, right.items) === ExtendsResult.True) + ) +} +// prettier-ignore +function FromTupleRight(left: TSchema, right: TTuple) { + return ( + TypeGuard.IsNever(left) ? ExtendsResult.True : + TypeGuard.IsUnknown(left) ? ExtendsResult.False : + TypeGuard.IsAny(left) ? ExtendsResult.Union : + ExtendsResult.False + ) +} +// prettier-ignore +function FromTuple(left: TTuple, right: TSchema): ExtendsResult { + return ( + IsStructuralRight(right) ? StructuralRight(left, right) : + TypeGuard.IsObject(right) && IsObjectArrayLike(right) ? ExtendsResult.True : + TypeGuard.IsArray(right) && IsArrayOfTuple(left, right) ? ExtendsResult.True : + !TypeGuard.IsTuple(right) ? ExtendsResult.False : + (ValueGuard.IsUndefined(left.items) && !ValueGuard.IsUndefined(right.items)) || (!ValueGuard.IsUndefined(left.items) && ValueGuard.IsUndefined(right.items)) ? ExtendsResult.False : + (ValueGuard.IsUndefined(left.items) && !ValueGuard.IsUndefined(right.items)) ? ExtendsResult.True : + left.items!.every((schema, index) => Visit(schema, right.items![index]) === ExtendsResult.True) ? ExtendsResult.True : + ExtendsResult.False + ) +} +// ------------------------------------------------------------------ +// Uint8Array +// ------------------------------------------------------------------ +// prettier-ignore +function FromUint8Array(left: TUint8Array, right: TSchema) { + return ( + IsStructuralRight(right) ? StructuralRight(left, right) : + TypeGuard.IsObject(right) ? FromObjectRight(left, right) : + TypeGuard.IsRecord(right) ? FromRecordRight(left, right) : + TypeGuard.IsUint8Array(right) ? ExtendsResult.True : + ExtendsResult.False + ) +} +// ------------------------------------------------------------------ +// Undefined +// ------------------------------------------------------------------ +// prettier-ignore +function FromUndefined(left: TUndefined, right: TSchema) { + return ( + IsStructuralRight(right) ? StructuralRight(left, right) : + TypeGuard.IsObject(right) ? FromObjectRight(left, right) : + TypeGuard.IsRecord(right) ? FromRecordRight(left, right) : + TypeGuard.IsVoid(right) ? FromVoidRight(left, right) : + TypeGuard.IsUndefined(right) ? ExtendsResult.True : + ExtendsResult.False + ) +} +// ------------------------------------------------------------------ +// Union +// ------------------------------------------------------------------ +// prettier-ignore +function FromUnionRight(left: TSchema, right: TUnion): ExtendsResult { + return right.anyOf.some((schema) => Visit(left, schema) === ExtendsResult.True) + ? ExtendsResult.True + : ExtendsResult.False +} +// prettier-ignore +function FromUnion(left: TUnion, right: TSchema): ExtendsResult { + return left.anyOf.every((schema) => Visit(schema, right) === ExtendsResult.True) + ? ExtendsResult.True + : ExtendsResult.False +} +// ------------------------------------------------------------------ +// Unknown +// ------------------------------------------------------------------ +// prettier-ignore +function FromUnknownRight(left: TSchema, right: TUnknown) { + return ExtendsResult.True +} +// prettier-ignore +function FromUnknown(left: TUnknown, right: TSchema) { + return ( + TypeGuard.IsNever(right) ? FromNeverRight(left, right) : + TypeGuard.IsIntersect(right) ? FromIntersectRight(left, right) : + TypeGuard.IsUnion(right) ? FromUnionRight(left, right) : + TypeGuard.IsAny(right) ? FromAnyRight(left, right) : + TypeGuard.IsString(right) ? FromStringRight(left, right) : + TypeGuard.IsNumber(right) ? FromNumberRight(left, right) : + TypeGuard.IsInteger(right) ? FromIntegerRight(left, right) : + TypeGuard.IsBoolean(right) ? FromBooleanRight(left, right) : + TypeGuard.IsArray(right) ? FromArrayRight(left, right) : + TypeGuard.IsTuple(right) ? FromTupleRight(left, right) : + TypeGuard.IsObject(right) ? FromObjectRight(left, right) : + TypeGuard.IsUnknown(right) ? ExtendsResult.True : + ExtendsResult.False + ) +} +// ------------------------------------------------------------------ +// Void +// ------------------------------------------------------------------ +// prettier-ignore +function FromVoidRight(left: TSchema, right: TVoid) { + return ( + TypeGuard.IsUndefined(left) ? ExtendsResult.True : + TypeGuard.IsUndefined(left) ? ExtendsResult.True : + ExtendsResult.False + ) +} +// prettier-ignore +function FromVoid(left: TVoid, right: TSchema) { + return ( + TypeGuard.IsIntersect(right) ? FromIntersectRight(left, right) : + TypeGuard.IsUnion(right) ? FromUnionRight(left, right) : + TypeGuard.IsUnknown(right) ? FromUnknownRight(left, right) : + TypeGuard.IsAny(right) ? FromAnyRight(left, right) : + TypeGuard.IsObject(right) ? FromObjectRight(left, right) : + TypeGuard.IsVoid(right) ? ExtendsResult.True : + ExtendsResult.False + ) +} +// prettier-ignore +function Visit(left: TSchema, right: TSchema): ExtendsResult { + return ( + // resolvable + (TypeGuard.IsTemplateLiteral(left) || TypeGuard.IsTemplateLiteral(right)) ? FromTemplateLiteral(left, right) : + (TypeGuard.IsRegExp(left) || TypeGuard.IsRegExp(right)) ? FromRegExp(left, right) : + (TypeGuard.IsNot(left) || TypeGuard.IsNot(right)) ? FromNot(left, right) : + // standard + TypeGuard.IsAny(left) ? FromAny(left, right) : + TypeGuard.IsArray(left) ? FromArray(left, right) : + TypeGuard.IsBigInt(left) ? FromBigInt(left, right) : + TypeGuard.IsBoolean(left) ? FromBoolean(left, right) : + TypeGuard.IsAsyncIterator(left) ? FromAsyncIterator(left, right) : + TypeGuard.IsConstructor(left) ? FromConstructor(left, right) : + TypeGuard.IsDate(left) ? FromDate(left, right) : + TypeGuard.IsFunction(left) ? FromFunction(left, right) : + TypeGuard.IsInteger(left) ? FromInteger(left, right) : + TypeGuard.IsIntersect(left) ? FromIntersect(left, right) : + TypeGuard.IsIterator(left) ? FromIterator(left, right) : + TypeGuard.IsLiteral(left) ? FromLiteral(left, right) : + TypeGuard.IsNever(left) ? FromNever(left, right) : + TypeGuard.IsNull(left) ? FromNull(left, right) : + TypeGuard.IsNumber(left) ? FromNumber(left, right) : + TypeGuard.IsObject(left) ? FromObject(left, right) : + TypeGuard.IsRecord(left) ? FromRecord(left, right) : + TypeGuard.IsString(left) ? FromString(left, right) : + TypeGuard.IsSymbol(left) ? FromSymbol(left, right) : + TypeGuard.IsTuple(left) ? FromTuple(left, right) : + TypeGuard.IsPromise(left) ? FromPromise(left, right) : + TypeGuard.IsUint8Array(left) ? FromUint8Array(left, right) : + TypeGuard.IsUndefined(left) ? FromUndefined(left, right) : + TypeGuard.IsUnion(left) ? FromUnion(left, right) : + TypeGuard.IsUnknown(left) ? FromUnknown(left, right) : + TypeGuard.IsVoid(left) ? FromVoid(left, right) : + Throw(`Unknown left type operand '${left[Kind]}'`) + ) +} +export function ExtendsCheck(left: TSchema, right: TSchema): ExtendsResult { + return Visit(left, right) +} diff --git a/src/type/extends/extends-from-mapped-key.ts b/src/type/extends/extends-from-mapped-key.ts new file mode 100644 index 000000000..697a2d37f --- /dev/null +++ b/src/type/extends/extends-from-mapped-key.ts @@ -0,0 +1,130 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { TSchema, SchemaOptions } from '../schema/index' +import type { TProperties } from '../object/index' +import type { Assert } from '../helpers/index' +import { MappedResult, type TMappedResult, type TMappedKey } from '../mapped/index' +import { Literal, type TLiteral, type TLiteralValue } from '../literal/index' +import { Extends, type TExtends } from './extends' +import { Clone } from '../clone/value' + +// ------------------------------------------------------------------ +// FromPropertyKey +// ------------------------------------------------------------------ +// prettier-ignore +type TFromPropertyKey< + K extends PropertyKey, + U extends TSchema, + L extends TSchema, + R extends TSchema +> = { + [_ in K]: TExtends>, U, L, R> + } +// prettier-ignore +function FromPropertyKey< + K extends PropertyKey, + U extends TSchema, + L extends TSchema, + R extends TSchema +>(K: K, U: U, L: L, R: R, options?: SchemaOptions): TFromPropertyKey { + return { + [K]: Extends(Literal(K as TLiteralValue), U, L, R, Clone(options)) as any + } as never +} +// ------------------------------------------------------------------ +// FromPropertyKeys +// ------------------------------------------------------------------ +// prettier-ignore +type TFromPropertyKeys< + K extends PropertyKey[], + U extends TSchema, + L extends TSchema, + R extends TSchema, + Acc extends TProperties = {} +> = ( + K extends [infer LK extends PropertyKey, ...infer RK extends PropertyKey[]] + ? TFromPropertyKeys> + : Acc +) +// prettier-ignore +function FromPropertyKeys< + K extends PropertyKey[], + U extends TSchema, + L extends TSchema, + R extends TSchema +>(K: [...K], U: U, L: L, R: R, options?: SchemaOptions): TFromPropertyKeys { + return K.reduce((Acc, LK) => { + return { ...Acc, ...FromPropertyKey(LK, U, L, R, options) } + }, {} as TProperties) as never +} +// ------------------------------------------------------------------ +// FromMappedKey +// ------------------------------------------------------------------ +// prettier-ignore +type TFromMappedKey< + K extends TMappedKey, + U extends TSchema, + L extends TSchema, + R extends TSchema +> = ( + TFromPropertyKeys + ) +// prettier-ignore +function FromMappedKey< + K extends TMappedKey, + U extends TSchema, + L extends TSchema, + R extends TSchema +>(K: K, U: U, L: L, R: R, options?: SchemaOptions): TFromMappedKey { + return FromPropertyKeys(K.keys, U, L, R, options) as never +} +// ------------------------------------------------------------------ +// ExtendsFromMappedKey +// ------------------------------------------------------------------ +// prettier-ignore +export type TExtendsFromMappedKey< + T extends TMappedKey, + U extends TSchema, + L extends TSchema, + R extends TSchema, + P extends TProperties = TFromMappedKey +> = ( + TMappedResult

+) +// prettier-ignore +export function ExtendsFromMappedKey< + T extends TMappedKey, + U extends TSchema, + L extends TSchema, + R extends TSchema, + P extends TProperties = TFromMappedKey +>(T: T, U: U, L: L, R: R, options?: SchemaOptions): TMappedResult

{ + const P = FromMappedKey(T, U, L, R, options) + return MappedResult(P) as never +} diff --git a/src/type/extends/extends-from-mapped-result.ts b/src/type/extends/extends-from-mapped-result.ts new file mode 100644 index 000000000..a7f4fb40b --- /dev/null +++ b/src/type/extends/extends-from-mapped-result.ts @@ -0,0 +1,102 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { TSchema, SchemaOptions } from '../schema/index' +import type { TProperties } from '../object/index' +import { MappedResult, type TMappedResult } from '../mapped/index' +import { Extends, type TExtends } from './extends' +import { Clone } from '../clone/value' + +// ------------------------------------------------------------------ +// FromProperties +// ------------------------------------------------------------------ +// prettier-ignore +type TFromProperties< + P extends TProperties, + Right extends TSchema, + False extends TSchema, + True extends TSchema +> = ( + { [K2 in keyof P]: TExtends } +) +// prettier-ignore +function FromProperties< + P extends TProperties, + Right extends TSchema, + True extends TSchema, + False extends TSchema +>(P: P, Right: Right, True: True, False: False, options?: SchemaOptions): TFromProperties { + const Acc = {} as TProperties + for(const K2 of globalThis.Object.getOwnPropertyNames(P)) Acc[K2] = Extends(P[K2], Right, True, False, Clone(options)) + return Acc as never +} +// ------------------------------------------------------------------ +// FromMappedResult +// ------------------------------------------------------------------ +// prettier-ignore +type TFromMappedResult< + Left extends TMappedResult, + Right extends TSchema, + True extends TSchema, + False extends TSchema +> = ( + TFromProperties +) +// prettier-ignore +function FromMappedResult< + Left extends TMappedResult, + Right extends TSchema, + True extends TSchema, + False extends TSchema +>(Left: Left, Right: Right, True: True, False: False, options?: SchemaOptions): TFromMappedResult { + return FromProperties(Left.properties, Right, True, False, options) as never +} +// ------------------------------------------------------------------ +// ExtendsFromMappedResult +// ------------------------------------------------------------------ +// prettier-ignore +export type TExtendsFromMappedResult< + Left extends TMappedResult, + Right extends TSchema, + True extends TSchema, + False extends TSchema, + P extends TProperties = TFromMappedResult +> = ( + TMappedResult

+) +// prettier-ignore +export function ExtendsFromMappedResult< + Left extends TMappedResult, + Right extends TSchema, + True extends TSchema, + False extends TSchema, + P extends TProperties = TFromMappedResult +>(Left: Left, Right: Right, True: True, False: False, options?: SchemaOptions): TMappedResult

{ + const P = FromMappedResult(Left, Right, True, False, options) + return MappedResult(P) as never +} diff --git a/src/type/extends/extends-undefined.ts b/src/type/extends/extends-undefined.ts new file mode 100644 index 000000000..4d6f41cd9 --- /dev/null +++ b/src/type/extends/extends-undefined.ts @@ -0,0 +1,55 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { TSchema } from '../schema/index' +import type { TIntersect } from '../intersect/index' +import type { TUnion } from '../union/index' +import type { TNot } from '../not/index' +import { Kind } from '../symbols/index' + +/** Fast undefined check used for properties of type undefined */ +function Intersect(schema: TIntersect) { + return schema.allOf.every((schema) => ExtendsUndefinedCheck(schema)) +} +function Union(schema: TUnion) { + return schema.anyOf.some((schema) => ExtendsUndefinedCheck(schema)) +} +function Not(schema: TNot) { + return !ExtendsUndefinedCheck(schema.not) +} +/** Fast undefined check used for properties of type undefined */ +// prettier-ignore +export function ExtendsUndefinedCheck(schema: TSchema): boolean { + return ( + schema[Kind] === 'Intersect' ? Intersect(schema as TIntersect) : + schema[Kind] === 'Union' ? Union(schema as TUnion) : + schema[Kind] === 'Not' ? Not(schema as TNot) : + schema[Kind] === 'Undefined' ? true : + false + ) +} diff --git a/src/type/extends/extends.ts b/src/type/extends/extends.ts new file mode 100644 index 000000000..ae9d264a7 --- /dev/null +++ b/src/type/extends/extends.ts @@ -0,0 +1,80 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { CreateType } from '../create/type' +import type { TSchema, SchemaOptions } from '../schema/index' +import type { Static } from '../static/index' +import { type TUnion, Union } from '../union/index' +import { TMappedKey, TMappedResult } from '../mapped/index' +import { ExtendsCheck, ExtendsResult } from './extends-check' +import { UnionToTuple } from '../helpers/index' +import { ExtendsFromMappedKey, type TExtendsFromMappedKey } from './extends-from-mapped-key' +import { ExtendsFromMappedResult, type TExtendsFromMappedResult } from './extends-from-mapped-result' + +// ------------------------------------------------------------------ +// TypeGuard +// ------------------------------------------------------------------ +import { IsMappedKey, IsMappedResult } from '../guard/kind' + +// prettier-ignore +type TExtendsResolve = ( + (Static extends Static ? T : U) extends infer O extends TSchema ? + UnionToTuple extends [infer X extends TSchema, infer Y extends TSchema] + ? TUnion<[X, Y]> + : O + : never +) +// prettier-ignore +function ExtendsResolve(left: L, right: R, trueType: T, falseType: U): TExtendsResolve { + const R = ExtendsCheck(left, right) + return ( + R === ExtendsResult.Union ? Union([trueType, falseType]) : + R === ExtendsResult.True ? trueType : + falseType + ) as never +} +// ------------------------------------------------------------------ +// TExtends +// ------------------------------------------------------------------ +export type TExtends = TExtendsResolve + +/** `[Json]` Creates a Conditional type */ +export function Extends(L: L, R: R, T: T, F: F, options?: SchemaOptions): TExtendsFromMappedResult +/** `[Json]` Creates a Conditional type */ +export function Extends(L: L, R: R, T: T, F: F, options?: SchemaOptions): TExtendsFromMappedKey +/** `[Json]` Creates a Conditional type */ +export function Extends(L: L, R: R, T: T, F: F, options?: SchemaOptions): TExtends +/** `[Json]` Creates a Conditional type */ +export function Extends(L: L, R: R, T: T, F: F, options?: SchemaOptions) { + // prettier-ignore + return ( + IsMappedResult(L) ? ExtendsFromMappedResult(L, R, T, F, options) : + IsMappedKey(L) ? CreateType(ExtendsFromMappedKey(L, R, T, F, options)) : + CreateType(ExtendsResolve(L, R, T, F), options) + ) as never +} diff --git a/src/type/extends/index.ts b/src/type/extends/index.ts new file mode 100644 index 000000000..1a8957763 --- /dev/null +++ b/src/type/extends/index.ts @@ -0,0 +1,33 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './extends-check' +export * from './extends-from-mapped-key' +export * from './extends-from-mapped-result' +export * from './extends-undefined' +export * from './extends' diff --git a/src/type/extract/extract-from-mapped-result.ts b/src/type/extract/extract-from-mapped-result.ts new file mode 100644 index 000000000..640ee37dd --- /dev/null +++ b/src/type/extract/extract-from-mapped-result.ts @@ -0,0 +1,89 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { TSchema } from '../schema/index' +import type { TProperties } from '../object/index' +import { MappedResult, type TMappedResult } from '../mapped/index' +import { Extract, type TExtract } from './extract' + +// ------------------------------------------------------------------ +// FromProperties +// ------------------------------------------------------------------ +// prettier-ignore +type TFromProperties< + P extends TProperties, + T extends TSchema +> = ( + { [K2 in keyof P]: TExtract } +) +// prettier-ignore +function FromProperties< + P extends TProperties, + T extends TSchema +>(P: P, T: T): TFromProperties { + const Acc = {} as TProperties + for(const K2 of globalThis.Object.getOwnPropertyNames(P)) Acc[K2] = Extract(P[K2], T) + return Acc as never +} +// ------------------------------------------------------------------ +// FromMappedResult +// ------------------------------------------------------------------ +// prettier-ignore +type TFromMappedResult< + R extends TMappedResult, + T extends TSchema +> = ( + TFromProperties +) +// prettier-ignore +function FromMappedResult< + R extends TMappedResult, + T extends TSchema +>(R: R, T: T): TFromMappedResult { + return FromProperties(R.properties, T) as never +} +// ------------------------------------------------------------------ +// ExtractFromMappedResult +// ------------------------------------------------------------------ +// prettier-ignore +export type TExtractFromMappedResult< + R extends TMappedResult, + T extends TSchema, + P extends TProperties = TFromMappedResult +> = ( + TMappedResult

+) +// prettier-ignore +export function ExtractFromMappedResult< + R extends TMappedResult, + T extends TSchema, + P extends TProperties = TFromMappedResult +>(R: R, T: T): TMappedResult

{ + const P = FromMappedResult(R, T) + return MappedResult(P) as never +} diff --git a/src/type/extract/extract-from-template-literal.ts b/src/type/extract/extract-from-template-literal.ts new file mode 100644 index 000000000..e3e9f8024 --- /dev/null +++ b/src/type/extract/extract-from-template-literal.ts @@ -0,0 +1,39 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { TSchema } from '../schema/index' +import { Extract, type TExtract } from './extract' +import { TemplateLiteralToUnion, type TTemplateLiteral, type TTemplateLiteralToUnion } from '../template-literal/index' + +// prettier-ignore +export type TExtractFromTemplateLiteral = ( + TExtract, R> +) +export function ExtractFromTemplateLiteral(L: L, R: R): TExtractFromTemplateLiteral { + return Extract(TemplateLiteralToUnion(L), R) as never +} diff --git a/src/type/extract/extract.ts b/src/type/extract/extract.ts new file mode 100644 index 000000000..aaed115ee --- /dev/null +++ b/src/type/extract/extract.ts @@ -0,0 +1,83 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { CreateType } from '../create/type' +import type { TSchema, SchemaOptions } from '../schema/index' +import type { AssertRest, AssertType, UnionToTuple } from '../helpers/index' +import type { TMappedResult } from '../mapped/index' +import { Union, type TUnion } from '../union/index' +import { type Static } from '../static/index' +import { Never, type TNever } from '../never/index' +import { type TUnionEvaluated } from '../union/index' +import { type TTemplateLiteral } from '../template-literal/index' +import { ExtendsCheck, ExtendsResult } from '../extends/index' +import { ExtractFromMappedResult, type TExtractFromMappedResult } from './extract-from-mapped-result' +import { ExtractFromTemplateLiteral, type TExtractFromTemplateLiteral } from './extract-from-template-literal' + +// ------------------------------------------------------------------ +// TypeGuard +// ------------------------------------------------------------------ +import { IsMappedResult, IsTemplateLiteral, IsUnion } from '../guard/kind' + +// ------------------------------------------------------------------ +// ExtractRest +// ------------------------------------------------------------------ +// prettier-ignore +type TExtractRest = AssertRest> extends Static ? L[K] : never + }[number]>> extends infer R extends TSchema[] ? TUnionEvaluated : never + +function ExtractRest(L: [...L], R: R) { + const extracted = L.filter((inner) => ExtendsCheck(inner, R) !== ExtendsResult.False) + return extracted.length === 1 ? extracted[0] : Union(extracted) +} +// ------------------------------------------------------------------ +// TExtract +// ------------------------------------------------------------------ +// prettier-ignore +export type TExtract = ( + L extends TUnion ? TExtractRest : + L extends U ? L : TNever +) +/** `[Json]` Constructs a type by extracting from type all union members that are assignable to union */ +export function Extract(type: L, union: R, options?: SchemaOptions): TExtractFromMappedResult +/** `[Json]` Constructs a type by extracting from type all union members that are assignable to union */ +export function Extract(type: L, union: R, options?: SchemaOptions): TExtractFromTemplateLiteral +/** `[Json]` Constructs a type by extracting from type all union members that are assignable to union */ +export function Extract(type: L, union: R, options?: SchemaOptions): TExtract +/** `[Json]` Constructs a type by extracting from type all union members that are assignable to union */ +export function Extract(L: TSchema, R: TSchema, options?: SchemaOptions): never { + // overloads + if (IsTemplateLiteral(L)) return CreateType(ExtractFromTemplateLiteral(L, R), options) as never + if (IsMappedResult(L)) return CreateType(ExtractFromMappedResult(L, R), options) as never + // prettier-ignore + return CreateType( + IsUnion(L) ? ExtractRest(L.anyOf, R) : + ExtendsCheck(L, R) !== ExtendsResult.False ? L : Never() + , options) as never +} diff --git a/src/type/extract/index.ts b/src/type/extract/index.ts new file mode 100644 index 000000000..c8f052d88 --- /dev/null +++ b/src/type/extract/index.ts @@ -0,0 +1,31 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './extract-from-mapped-result' +export * from './extract-from-template-literal' +export * from './extract' diff --git a/src/type/function/function.ts b/src/type/function/function.ts new file mode 100644 index 000000000..6feffaed4 --- /dev/null +++ b/src/type/function/function.ts @@ -0,0 +1,71 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { CreateType } from '../create/type' +import type { TSchema, SchemaOptions } from '../schema/index' +import type { Static } from '../static/index' +import type { Ensure } from '../helpers/index' +import type { TReadonlyOptional } from '../readonly-optional/index' +import type { TReadonly } from '../readonly/index' +import type { TOptional } from '../optional/index' +import { Kind } from '../symbols/index' + +// ------------------------------------------------------------------ +// StaticFunction +// ------------------------------------------------------------------ +type StaticReturnType = Static +// prettier-ignore +type StaticParameter = + T extends TReadonlyOptional ? [Readonly>?] : + T extends TReadonly ? [Readonly>] : + T extends TOptional ? [Static?] : + [Static] +// prettier-ignore +type StaticParameters = ( + T extends [infer L extends TSchema, ...infer R extends TSchema[]] + ? StaticParameters]> + : Acc +) +// prettier-ignore +type StaticFunction = + Ensure<(...param: StaticParameters) => StaticReturnType> + +// ------------------------------------------------------------------ +// TFunction +// ------------------------------------------------------------------ +export interface TFunction extends TSchema { + [Kind]: 'Function' + static: StaticFunction + type: 'Function' + parameters: T + returns: U +} +/** `[JavaScript]` Creates a Function type */ +export function Function(parameters: [...T], returns: U, options?: SchemaOptions): TFunction { + return CreateType({ [Kind]: 'Function', type: 'Function', parameters, returns }, options) as never +} diff --git a/src/type/function/index.ts b/src/type/function/index.ts new file mode 100644 index 000000000..948381971 --- /dev/null +++ b/src/type/function/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './function' diff --git a/src/type/guard/index.ts b/src/type/guard/index.ts new file mode 100644 index 000000000..9745d55cd --- /dev/null +++ b/src/type/guard/index.ts @@ -0,0 +1,31 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * as KindGuard from './kind' +export * as TypeGuard from './type' +export * as ValueGuard from './value' diff --git a/src/type/guard/kind.ts b/src/type/guard/kind.ts new file mode 100644 index 000000000..6447412f4 --- /dev/null +++ b/src/type/guard/kind.ts @@ -0,0 +1,307 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import * as ValueGuard from './value' +import { Kind, Hint, TransformKind, ReadonlyKind, OptionalKind } from '../symbols/index' +import { TransformOptions } from '../transform/index' + +import type { TAny } from '../any/index' +import type { TArgument } from '../argument/index' +import type { TArray } from '../array/index' +import type { TAsyncIterator } from '../async-iterator/index' +import type { TBoolean } from '../boolean/index' +import type { TComputed } from '../computed/index' +import type { TBigInt } from '../bigint/index' +import type { TConstructor } from '../constructor/index' +import type { TFunction } from '../function/index' +import type { TImport } from '../module/index' +import type { TInteger } from '../integer/index' +import type { TIntersect } from '../intersect/index' +import type { TIterator } from '../iterator/index' +import type { TLiteral, TLiteralValue } from '../literal/index' +import type { TMappedKey, TMappedResult } from '../mapped/index' +import type { TNever } from '../never/index' +import type { TNot } from '../not/index' +import type { TNull } from '../null/index' +import type { TNumber } from '../number/index' +import type { TObject, TProperties } from '../object/index' +import type { TOptional } from '../optional/index' +import type { TPromise } from '../promise/index' +import type { TReadonly } from '../readonly/index' +import type { TRecord } from '../record/index' +import type { TRef } from '../ref/index' +import type { TRegExp } from '../regexp/index' +import type { TSchema } from '../schema/index' +import type { TString } from '../string/index' +import type { TSymbol } from '../symbol/index' +import type { TTemplateLiteral } from '../template-literal/index' +import type { TTuple } from '../tuple/index' +import type { TUint8Array } from '../uint8array/index' +import type { TUndefined } from '../undefined/index' +import type { TUnknown } from '../unknown/index' +import type { TUnion } from '../union/index' +import type { TUnsafe } from '../unsafe/index' +import type { TVoid } from '../void/index' +import type { TDate } from '../date/index' +import type { TThis } from '../recursive/index' + +/** `[Kind-Only]` Returns true if this value has a Readonly symbol */ +export function IsReadonly(value: T): value is TReadonly { + return ValueGuard.IsObject(value) && value[ReadonlyKind] === 'Readonly' +} +/** `[Kind-Only]` Returns true if this value has a Optional symbol */ +export function IsOptional(value: T): value is TOptional { + return ValueGuard.IsObject(value) && value[OptionalKind] === 'Optional' +} +/** `[Kind-Only]` Returns true if the given value is TAny */ +export function IsAny(value: unknown): value is TAny { + return IsKindOf(value, 'Any') +} +/** `[Kind-Only]` Returns true if the given value is TArgument */ +export function IsArgument(value: unknown): value is TArgument { + return IsKindOf(value, 'Argument') +} +/** `[Kind-Only]` Returns true if the given value is TArray */ +export function IsArray(value: unknown): value is TArray { + return IsKindOf(value, 'Array') +} +/** `[Kind-Only]` Returns true if the given value is TAsyncIterator */ +export function IsAsyncIterator(value: unknown): value is TAsyncIterator { + return IsKindOf(value, 'AsyncIterator') +} +/** `[Kind-Only]` Returns true if the given value is TBigInt */ +export function IsBigInt(value: unknown): value is TBigInt { + return IsKindOf(value, 'BigInt') +} +/** `[Kind-Only]` Returns true if the given value is TBoolean */ +export function IsBoolean(value: unknown): value is TBoolean { + return IsKindOf(value, 'Boolean') +} +/** `[Kind-Only]` Returns true if the given value is TComputed */ +export function IsComputed(value: unknown): value is TComputed { + return IsKindOf(value, 'Computed') +} +/** `[Kind-Only]` Returns true if the given value is TConstructor */ +export function IsConstructor(value: unknown): value is TConstructor { + return IsKindOf(value, 'Constructor') +} +/** `[Kind-Only]` Returns true if the given value is TDate */ +export function IsDate(value: unknown): value is TDate { + return IsKindOf(value, 'Date') +} +/** `[Kind-Only]` Returns true if the given value is TFunction */ +export function IsFunction(value: unknown): value is TFunction { + return IsKindOf(value, 'Function') +} +/** `[Kind-Only]` Returns true if the given value is TInteger */ +export function IsImport(value: unknown): value is TImport { + return IsKindOf(value, 'Import') +} +/** `[Kind-Only]` Returns true if the given value is TInteger */ +export function IsInteger(value: unknown): value is TInteger { + return IsKindOf(value, 'Integer') +} +/** `[Kind-Only]` Returns true if the given schema is TProperties */ +export function IsProperties(value: unknown): value is TProperties { + return ValueGuard.IsObject(value) +} +/** `[Kind-Only]` Returns true if the given value is TIntersect */ +export function IsIntersect(value: unknown): value is TIntersect { + return IsKindOf(value, 'Intersect') +} +/** `[Kind-Only]` Returns true if the given value is TIterator */ +export function IsIterator(value: unknown): value is TIterator { + return IsKindOf(value, 'Iterator') +} +/** `[Kind-Only]` Returns true if the given value is a TKind with the given name. */ +export function IsKindOf(value: unknown, kind: T): value is Record & { [Kind]: T } { + return ValueGuard.IsObject(value) && Kind in value && value[Kind] === kind +} +/** `[Kind-Only]` Returns true if the given value is TLiteral */ +export function IsLiteralString(value: unknown): value is TLiteral { + return IsLiteral(value) && ValueGuard.IsString(value.const) +} +/** `[Kind-Only]` Returns true if the given value is TLiteral */ +export function IsLiteralNumber(value: unknown): value is TLiteral { + return IsLiteral(value) && ValueGuard.IsNumber(value.const) +} +/** `[Kind-Only]` Returns true if the given value is TLiteral */ +export function IsLiteralBoolean(value: unknown): value is TLiteral { + return IsLiteral(value) && ValueGuard.IsBoolean(value.const) +} +/** `[Kind-Only]` Returns true if the given value is TLiteralValue */ +export function IsLiteralValue(value: unknown): value is TLiteralValue { + return ValueGuard.IsBoolean(value) || ValueGuard.IsNumber(value) || ValueGuard.IsString(value) +} +/** `[Kind-Only]` Returns true if the given value is TLiteral */ +export function IsLiteral(value: unknown): value is TLiteral { + return IsKindOf(value, 'Literal') +} +/** `[Kind-Only]` Returns true if the given value is a TMappedKey */ +export function IsMappedKey(value: unknown): value is TMappedKey { + return IsKindOf(value, 'MappedKey') +} +/** `[Kind-Only]` Returns true if the given value is TMappedResult */ +export function IsMappedResult(value: unknown): value is TMappedResult { + return IsKindOf(value, 'MappedResult') +} +/** `[Kind-Only]` Returns true if the given value is TNever */ +export function IsNever(value: unknown): value is TNever { + return IsKindOf(value, 'Never') +} +/** `[Kind-Only]` Returns true if the given value is TNot */ +export function IsNot(value: unknown): value is TNot { + return IsKindOf(value, 'Not') +} +/** `[Kind-Only]` Returns true if the given value is TNull */ +export function IsNull(value: unknown): value is TNull { + return IsKindOf(value, 'Null') +} +/** `[Kind-Only]` Returns true if the given value is TNumber */ +export function IsNumber(value: unknown): value is TNumber { + return IsKindOf(value, 'Number') +} +/** `[Kind-Only]` Returns true if the given value is TObject */ +export function IsObject(value: unknown): value is TObject { + return IsKindOf(value, 'Object') +} +/** `[Kind-Only]` Returns true if the given value is TPromise */ +export function IsPromise(value: unknown): value is TPromise { + return IsKindOf(value, 'Promise') +} +/** `[Kind-Only]` Returns true if the given value is TRecord */ +export function IsRecord(value: unknown): value is TRecord { + return IsKindOf(value, 'Record') +} +/** `[Kind-Only]` Returns true if this value is TRecursive */ +export function IsRecursive(value: unknown): value is { [Hint]: 'Recursive' } { + return ValueGuard.IsObject(value) && Hint in value && value[Hint] === 'Recursive' +} +/** `[Kind-Only]` Returns true if the given value is TRef */ +export function IsRef(value: unknown): value is TRef { + return IsKindOf(value, 'Ref') +} +/** `[Kind-Only]` Returns true if the given value is TRegExp */ +export function IsRegExp(value: unknown): value is TRegExp { + return IsKindOf(value, 'RegExp') +} +/** `[Kind-Only]` Returns true if the given value is TString */ +export function IsString(value: unknown): value is TString { + return IsKindOf(value, 'String') +} +/** `[Kind-Only]` Returns true if the given value is TSymbol */ +export function IsSymbol(value: unknown): value is TSymbol { + return IsKindOf(value, 'Symbol') +} +/** `[Kind-Only]` Returns true if the given value is TTemplateLiteral */ +export function IsTemplateLiteral(value: unknown): value is TTemplateLiteral { + return IsKindOf(value, 'TemplateLiteral') +} +/** `[Kind-Only]` Returns true if the given value is TThis */ +export function IsThis(value: unknown): value is TThis { + return IsKindOf(value, 'This') +} +/** `[Kind-Only]` Returns true of this value is TTransform */ +export function IsTransform(value: unknown): value is { [TransformKind]: TransformOptions } { + return ValueGuard.IsObject(value) && TransformKind in value +} +/** `[Kind-Only]` Returns true if the given value is TTuple */ +export function IsTuple(value: unknown): value is TTuple { + return IsKindOf(value, 'Tuple') +} +/** `[Kind-Only]` Returns true if the given value is TUndefined */ +export function IsUndefined(value: unknown): value is TUndefined { + return IsKindOf(value, 'Undefined') +} +/** `[Kind-Only]` Returns true if the given value is TUnion */ +export function IsUnion(value: unknown): value is TUnion { + return IsKindOf(value, 'Union') +} +/** `[Kind-Only]` Returns true if the given value is TUint8Array */ +export function IsUint8Array(value: unknown): value is TUint8Array { + return IsKindOf(value, 'Uint8Array') +} +/** `[Kind-Only]` Returns true if the given value is TUnknown */ +export function IsUnknown(value: unknown): value is TUnknown { + return IsKindOf(value, 'Unknown') +} +/** `[Kind-Only]` Returns true if the given value is a raw TUnsafe */ +export function IsUnsafe(value: unknown): value is TUnsafe { + return IsKindOf(value, 'Unsafe') +} +/** `[Kind-Only]` Returns true if the given value is TVoid */ +export function IsVoid(value: unknown): value is TVoid { + return IsKindOf(value, 'Void') +} +/** `[Kind-Only]` Returns true if the given value is TKind */ +export function IsKind(value: unknown): value is Record & { [Kind]: string } { + return ValueGuard.IsObject(value) && Kind in value && ValueGuard.IsString(value[Kind]) +} +/** `[Kind-Only]` Returns true if the given value is TSchema */ +export function IsSchema(value: unknown): value is TSchema { + // prettier-ignore + return ( + IsAny(value) || + IsArgument(value) || + IsArray(value) || + IsBoolean(value) || + IsBigInt(value) || + IsAsyncIterator(value) || + IsComputed(value) || + IsConstructor(value) || + IsDate(value) || + IsFunction(value) || + IsInteger(value) || + IsIntersect(value) || + IsIterator(value) || + IsLiteral(value) || + IsMappedKey(value) || + IsMappedResult(value) || + IsNever(value) || + IsNot(value) || + IsNull(value) || + IsNumber(value) || + IsObject(value) || + IsPromise(value) || + IsRecord(value) || + IsRef(value) || + IsRegExp(value) || + IsString(value) || + IsSymbol(value) || + IsTemplateLiteral(value) || + IsThis(value) || + IsTuple(value) || + IsUndefined(value) || + IsUnion(value) || + IsUint8Array(value) || + IsUnknown(value) || + IsUnsafe(value) || + IsVoid(value) || + IsKind(value) + ) +} diff --git a/src/type/guard/type.ts b/src/type/guard/type.ts new file mode 100644 index 000000000..9df35670a --- /dev/null +++ b/src/type/guard/type.ts @@ -0,0 +1,659 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import * as ValueGuard from './value' +import { Kind, Hint, TransformKind, ReadonlyKind, OptionalKind } from '../symbols/index' +import { TypeBoxError } from '../error/index' +import { TransformOptions } from '../transform/index' + +import type { TAny } from '../any/index' +import type { TArgument } from '../argument/index' +import type { TArray } from '../array/index' +import type { TAsyncIterator } from '../async-iterator/index' +import type { TBoolean } from '../boolean/index' +import type { TComputed } from '../computed/index' +import type { TBigInt } from '../bigint/index' +import type { TConstructor } from '../constructor/index' +import type { TFunction } from '../function/index' +import type { TImport } from '../module/index' +import type { TInteger } from '../integer/index' +import type { TIntersect } from '../intersect/index' +import type { TIterator } from '../iterator/index' +import type { TLiteral, TLiteralValue } from '../literal/index' +import type { TMappedKey, TMappedResult } from '../mapped/index' +import type { TNever } from '../never/index' +import type { TNot } from '../not/index' +import type { TNull } from '../null/index' +import type { TNumber } from '../number/index' +import type { TObject, TAdditionalProperties, TProperties } from '../object/index' +import type { TOptional } from '../optional/index' +import type { TPromise } from '../promise/index' +import type { TReadonly } from '../readonly/index' +import type { TRecord } from '../record/index' +import type { TRef } from '../ref/index' +import type { TRegExp } from '../regexp/index' +import type { TSchema } from '../schema/index' +import type { TString } from '../string/index' +import type { TSymbol } from '../symbol/index' +import type { TTemplateLiteral } from '../template-literal/index' +import type { TTuple } from '../tuple/index' +import type { TUint8Array } from '../uint8array/index' +import type { TUndefined } from '../undefined/index' +import type { TUnion } from '../union/index' +import type { TUnknown } from '../unknown/index' +import type { TUnsafe } from '../unsafe/index' +import type { TVoid } from '../void/index' +import type { TDate } from '../date/index' +import type { TThis } from '../recursive/index' + +export class TypeGuardUnknownTypeError extends TypeBoxError {} + +const KnownTypes = [ + 'Argument', + 'Any', + 'Array', + 'AsyncIterator', + 'BigInt', + 'Boolean', + 'Computed', + 'Constructor', + 'Date', + 'Enum', + 'Function', + 'Integer', + 'Intersect', + 'Iterator', + 'Literal', + 'MappedKey', + 'MappedResult', + 'Not', + 'Null', + 'Number', + 'Object', + 'Promise', + 'Record', + 'Ref', + 'RegExp', + 'String', + 'Symbol', + 'TemplateLiteral', + 'This', + 'Tuple', + 'Undefined', + 'Union', + 'Uint8Array', + 'Unknown', + 'Void', +] +function IsPattern(value: unknown): value is string { + try { + new RegExp(value as string) + return true + } catch { + return false + } +} +function IsControlCharacterFree(value: unknown): value is string { + if (!ValueGuard.IsString(value)) return false + for (let i = 0; i < value.length; i++) { + const code = value.charCodeAt(i) + if ((code >= 7 && code <= 13) || code === 27 || code === 127) { + return false + } + } + return true +} +function IsAdditionalProperties(value: unknown): value is TAdditionalProperties { + return IsOptionalBoolean(value) || IsSchema(value) +} +function IsOptionalBigInt(value: unknown): value is bigint | undefined { + return ValueGuard.IsUndefined(value) || ValueGuard.IsBigInt(value) +} +function IsOptionalNumber(value: unknown): value is number | undefined { + return ValueGuard.IsUndefined(value) || ValueGuard.IsNumber(value) +} +function IsOptionalBoolean(value: unknown): value is boolean | undefined { + return ValueGuard.IsUndefined(value) || ValueGuard.IsBoolean(value) +} +function IsOptionalString(value: unknown): value is string | undefined { + return ValueGuard.IsUndefined(value) || ValueGuard.IsString(value) +} +function IsOptionalPattern(value: unknown): value is string | undefined { + return ValueGuard.IsUndefined(value) || (ValueGuard.IsString(value) && IsControlCharacterFree(value) && IsPattern(value)) +} +function IsOptionalFormat(value: unknown): value is string | undefined { + return ValueGuard.IsUndefined(value) || (ValueGuard.IsString(value) && IsControlCharacterFree(value)) +} +function IsOptionalSchema(value: unknown): value is boolean | undefined { + return ValueGuard.IsUndefined(value) || IsSchema(value) +} +// ------------------------------------------------------------------ +// Modifiers +// ------------------------------------------------------------------ +/** Returns true if this value has a Readonly symbol */ +export function IsReadonly(value: T): value is TReadonly { + return ValueGuard.IsObject(value) && value[ReadonlyKind] === 'Readonly' +} +/** Returns true if this value has a Optional symbol */ +export function IsOptional(value: T): value is TOptional { + return ValueGuard.IsObject(value) && value[OptionalKind] === 'Optional' +} +// ------------------------------------------------------------------ +// Types +// ------------------------------------------------------------------ +/** Returns true if the given value is TAny */ +export function IsAny(value: unknown): value is TAny { + // prettier-ignore + return ( + IsKindOf(value, 'Any') && + IsOptionalString(value.$id) + ) +} +/** Returns true if the given value is TArgument */ +export function IsArgument(value: unknown): value is TArgument { + // prettier-ignore + return ( + IsKindOf(value, 'Argument') && + ValueGuard.IsNumber(value.index) + ) +} +/** Returns true if the given value is TArray */ +export function IsArray(value: unknown): value is TArray { + return ( + IsKindOf(value, 'Array') && + value.type === 'array' && + IsOptionalString(value.$id) && + IsSchema(value.items) && + IsOptionalNumber(value.minItems) && + IsOptionalNumber(value.maxItems) && + IsOptionalBoolean(value.uniqueItems) && + IsOptionalSchema(value.contains) && + IsOptionalNumber(value.minContains) && + IsOptionalNumber(value.maxContains) + ) +} +/** Returns true if the given value is TAsyncIterator */ +export function IsAsyncIterator(value: unknown): value is TAsyncIterator { + // prettier-ignore + return ( + IsKindOf(value, 'AsyncIterator') && + value.type === 'AsyncIterator' && + IsOptionalString(value.$id) && + IsSchema(value.items) + ) +} +/** Returns true if the given value is TBigInt */ +export function IsBigInt(value: unknown): value is TBigInt { + // prettier-ignore + return ( + IsKindOf(value, 'BigInt') && + value.type === 'bigint' && + IsOptionalString(value.$id) && + IsOptionalBigInt(value.exclusiveMaximum) && + IsOptionalBigInt(value.exclusiveMinimum) && + IsOptionalBigInt(value.maximum) && + IsOptionalBigInt(value.minimum) && + IsOptionalBigInt(value.multipleOf) + ) +} +/** Returns true if the given value is TBoolean */ +export function IsBoolean(value: unknown): value is TBoolean { + // prettier-ignore + return ( + IsKindOf(value, 'Boolean') && + value.type === 'boolean' && + IsOptionalString(value.$id) + ) +} +/** Returns true if the given value is TComputed */ +export function IsComputed(value: unknown): value is TComputed { + // prettier-ignore + return ( + IsKindOf(value, 'Computed') && + ValueGuard.IsString(value.target) && + ValueGuard.IsArray(value.parameters) && + value.parameters.every((schema) => IsSchema(schema)) + ) +} +/** Returns true if the given value is TConstructor */ +export function IsConstructor(value: unknown): value is TConstructor { + // prettier-ignore + return ( + IsKindOf(value, 'Constructor') && + value.type === 'Constructor' && + IsOptionalString(value.$id) && + ValueGuard.IsArray(value.parameters) && + value.parameters.every(schema => IsSchema(schema)) && + IsSchema(value.returns) + ) +} +/** Returns true if the given value is TDate */ +export function IsDate(value: unknown): value is TDate { + return ( + IsKindOf(value, 'Date') && + value.type === 'Date' && + IsOptionalString(value.$id) && + IsOptionalNumber(value.exclusiveMaximumTimestamp) && + IsOptionalNumber(value.exclusiveMinimumTimestamp) && + IsOptionalNumber(value.maximumTimestamp) && + IsOptionalNumber(value.minimumTimestamp) && + IsOptionalNumber(value.multipleOfTimestamp) + ) +} +/** Returns true if the given value is TFunction */ +export function IsFunction(value: unknown): value is TFunction { + // prettier-ignore + return ( + IsKindOf(value, 'Function') && + value.type === 'Function' && + IsOptionalString(value.$id) && + ValueGuard.IsArray(value.parameters) && + value.parameters.every(schema => IsSchema(schema)) && + IsSchema(value.returns) + ) +} +/** Returns true if the given value is TImport */ +export function IsImport(value: unknown): value is TImport { + // prettier-ignore + return ( + IsKindOf(value, 'Import') && + ValueGuard.HasPropertyKey(value, '$defs') && + ValueGuard.IsObject(value.$defs) && + IsProperties(value.$defs) && + ValueGuard.HasPropertyKey(value, '$ref') && + ValueGuard.IsString(value.$ref) && + value.$ref in value.$defs // required + ) +} +/** Returns true if the given value is TInteger */ +export function IsInteger(value: unknown): value is TInteger { + return ( + IsKindOf(value, 'Integer') && + value.type === 'integer' && + IsOptionalString(value.$id) && + IsOptionalNumber(value.exclusiveMaximum) && + IsOptionalNumber(value.exclusiveMinimum) && + IsOptionalNumber(value.maximum) && + IsOptionalNumber(value.minimum) && + IsOptionalNumber(value.multipleOf) + ) +} +/** Returns true if the given schema is TProperties */ +export function IsProperties(value: unknown): value is TProperties { + // prettier-ignore + return ( + ValueGuard.IsObject(value) && + Object.entries(value).every(([key, schema]) => IsControlCharacterFree(key) && IsSchema(schema)) + ) +} +/** Returns true if the given value is TIntersect */ +export function IsIntersect(value: unknown): value is TIntersect { + // prettier-ignore + return ( + IsKindOf(value, 'Intersect') && + (ValueGuard.IsString(value.type) && value.type !== 'object' ? false : true) && + ValueGuard.IsArray(value.allOf) && + value.allOf.every(schema => IsSchema(schema) && !IsTransform(schema)) && + IsOptionalString(value.type) && + (IsOptionalBoolean(value.unevaluatedProperties) || IsOptionalSchema(value.unevaluatedProperties)) && + IsOptionalString(value.$id) + ) +} +/** Returns true if the given value is TIterator */ +export function IsIterator(value: unknown): value is TIterator { + // prettier-ignore + return ( + IsKindOf(value, 'Iterator') && + value.type === 'Iterator' && + IsOptionalString(value.$id) && + IsSchema(value.items) + ) +} +/** Returns true if the given value is a TKind with the given name. */ +export function IsKindOf(value: unknown, kind: T): value is Record & { [Kind]: T } { + return ValueGuard.IsObject(value) && Kind in value && value[Kind] === kind +} +/** Returns true if the given value is TLiteral */ +export function IsLiteralString(value: unknown): value is TLiteral { + return IsLiteral(value) && ValueGuard.IsString(value.const) +} +/** Returns true if the given value is TLiteral */ +export function IsLiteralNumber(value: unknown): value is TLiteral { + return IsLiteral(value) && ValueGuard.IsNumber(value.const) +} +/** Returns true if the given value is TLiteral */ +export function IsLiteralBoolean(value: unknown): value is TLiteral { + return IsLiteral(value) && ValueGuard.IsBoolean(value.const) +} +/** Returns true if the given value is TLiteral */ +export function IsLiteral(value: unknown): value is TLiteral { + // prettier-ignore + return ( + IsKindOf(value, 'Literal') && + IsOptionalString(value.$id) && IsLiteralValue(value.const) + ) +} +/** Returns true if the given value is a TLiteralValue */ +export function IsLiteralValue(value: unknown): value is TLiteralValue { + return ValueGuard.IsBoolean(value) || ValueGuard.IsNumber(value) || ValueGuard.IsString(value) +} +/** Returns true if the given value is a TMappedKey */ +export function IsMappedKey(value: unknown): value is TMappedKey { + // prettier-ignore + return ( + IsKindOf(value, 'MappedKey') && + ValueGuard.IsArray(value.keys) && + value.keys.every(key => ValueGuard.IsNumber(key) || ValueGuard.IsString(key)) + ) +} +/** Returns true if the given value is TMappedResult */ +export function IsMappedResult(value: unknown): value is TMappedResult { + // prettier-ignore + return ( + IsKindOf(value, 'MappedResult') && + IsProperties(value.properties) + ) +} +/** Returns true if the given value is TNever */ +export function IsNever(value: unknown): value is TNever { + // prettier-ignore + return ( + IsKindOf(value, 'Never') && + ValueGuard.IsObject(value.not) && + Object.getOwnPropertyNames(value.not).length === 0 + ) +} +/** Returns true if the given value is TNot */ +export function IsNot(value: unknown): value is TNot { + // prettier-ignore + return ( + IsKindOf(value, 'Not') && + IsSchema(value.not) + ) +} +/** Returns true if the given value is TNull */ +export function IsNull(value: unknown): value is TNull { + // prettier-ignore + return ( + IsKindOf(value, 'Null') && + value.type === 'null' && + IsOptionalString(value.$id) + ) +} +/** Returns true if the given value is TNumber */ +export function IsNumber(value: unknown): value is TNumber { + return ( + IsKindOf(value, 'Number') && + value.type === 'number' && + IsOptionalString(value.$id) && + IsOptionalNumber(value.exclusiveMaximum) && + IsOptionalNumber(value.exclusiveMinimum) && + IsOptionalNumber(value.maximum) && + IsOptionalNumber(value.minimum) && + IsOptionalNumber(value.multipleOf) + ) +} +/** Returns true if the given value is TObject */ +export function IsObject(value: unknown): value is TObject { + // prettier-ignore + return ( + IsKindOf(value, 'Object') && + value.type === 'object' && + IsOptionalString(value.$id) && + IsProperties(value.properties) && + IsAdditionalProperties(value.additionalProperties) && + IsOptionalNumber(value.minProperties) && + IsOptionalNumber(value.maxProperties) + ) +} +/** Returns true if the given value is TPromise */ +export function IsPromise(value: unknown): value is TPromise { + // prettier-ignore + return ( + IsKindOf(value, 'Promise') && + value.type === 'Promise' && + IsOptionalString(value.$id) && + IsSchema(value.item) + ) +} +/** Returns true if the given value is TRecord */ +export function IsRecord(value: unknown): value is TRecord { + // prettier-ignore + return ( + IsKindOf(value, 'Record') && + value.type === 'object' && + IsOptionalString(value.$id) && + IsAdditionalProperties(value.additionalProperties) && + ValueGuard.IsObject(value.patternProperties) && + ((schema: Record) => { + const keys = Object.getOwnPropertyNames(schema.patternProperties) + return ( + keys.length === 1 && + IsPattern(keys[0]) && + ValueGuard.IsObject(schema.patternProperties) && + IsSchema(schema.patternProperties[keys[0]]) + ) + })(value) + ) +} +/** Returns true if this value is TRecursive */ +export function IsRecursive(value: unknown): value is { [Hint]: 'Recursive' } { + return ValueGuard.IsObject(value) && Hint in value && value[Hint] === 'Recursive' +} +/** Returns true if the given value is TRef */ +export function IsRef(value: unknown): value is TRef { + // prettier-ignore + return ( + IsKindOf(value, 'Ref') && + IsOptionalString(value.$id) && + ValueGuard.IsString(value.$ref) + ) +} +/** Returns true if the given value is TRegExp */ +export function IsRegExp(value: unknown): value is TRegExp { + // prettier-ignore + return ( + IsKindOf(value, 'RegExp') && + IsOptionalString(value.$id) && + ValueGuard.IsString(value.source) && + ValueGuard.IsString(value.flags) && + IsOptionalNumber(value.maxLength) && + IsOptionalNumber(value.minLength) + ) +} +/** Returns true if the given value is TString */ +export function IsString(value: unknown): value is TString { + // prettier-ignore + return ( + IsKindOf(value, 'String') && + value.type === 'string' && + IsOptionalString(value.$id) && + IsOptionalNumber(value.minLength) && + IsOptionalNumber(value.maxLength) && + IsOptionalPattern(value.pattern) && + IsOptionalFormat(value.format) + ) +} +/** Returns true if the given value is TSymbol */ +export function IsSymbol(value: unknown): value is TSymbol { + // prettier-ignore + return ( + IsKindOf(value, 'Symbol') && + value.type === 'symbol' && + IsOptionalString(value.$id) + ) +} +/** Returns true if the given value is TTemplateLiteral */ +export function IsTemplateLiteral(value: unknown): value is TTemplateLiteral { + // prettier-ignore + return ( + IsKindOf(value, 'TemplateLiteral') && + value.type === 'string' && + ValueGuard.IsString(value.pattern) && + value.pattern[0] === '^' && + value.pattern[value.pattern.length - 1] === '$' + ) +} +/** Returns true if the given value is TThis */ +export function IsThis(value: unknown): value is TThis { + // prettier-ignore + return ( + IsKindOf(value, 'This') && + IsOptionalString(value.$id) && + ValueGuard.IsString(value.$ref) + ) +} +/** Returns true of this value is TTransform */ +export function IsTransform(value: unknown): value is { [TransformKind]: TransformOptions } { + return ValueGuard.IsObject(value) && TransformKind in value +} +/** Returns true if the given value is TTuple */ +export function IsTuple(value: unknown): value is TTuple { + // prettier-ignore + return ( + IsKindOf(value, 'Tuple') && + value.type === 'array' && + IsOptionalString(value.$id) && + ValueGuard.IsNumber(value.minItems) && + ValueGuard.IsNumber(value.maxItems) && + value.minItems === value.maxItems && + (( // empty + ValueGuard.IsUndefined(value.items) && + ValueGuard.IsUndefined(value.additionalItems) && + value.minItems === 0 + ) || ( + ValueGuard.IsArray(value.items) && + value.items.every(schema => IsSchema(schema)) + )) + ) +} +/** Returns true if the given value is TUndefined */ +export function IsUndefined(value: unknown): value is TUndefined { + // prettier-ignore + return ( + IsKindOf(value, 'Undefined') && + value.type === 'undefined' && + IsOptionalString(value.$id) + ) +} +/** Returns true if the given value is TUnion[]> */ +export function IsUnionLiteral(value: unknown): value is TUnion { + return IsUnion(value) && value.anyOf.every((schema) => IsLiteralString(schema) || IsLiteralNumber(schema)) +} +/** Returns true if the given value is TUnion */ +export function IsUnion(value: unknown): value is TUnion { + // prettier-ignore + return ( + IsKindOf(value, 'Union') && + IsOptionalString(value.$id) && + ValueGuard.IsObject(value) && + ValueGuard.IsArray(value.anyOf) && + value.anyOf.every(schema => IsSchema(schema)) + ) +} +/** Returns true if the given value is TUint8Array */ +export function IsUint8Array(value: unknown): value is TUint8Array { + // prettier-ignore + return ( + IsKindOf(value, 'Uint8Array') && + value.type === 'Uint8Array' && + IsOptionalString(value.$id) && + IsOptionalNumber(value.minByteLength) && + IsOptionalNumber(value.maxByteLength) + ) +} +/** Returns true if the given value is TUnknown */ +export function IsUnknown(value: unknown): value is TUnknown { + // prettier-ignore + return ( + IsKindOf(value, 'Unknown') && + IsOptionalString(value.$id) + ) +} +/** Returns true if the given value is a raw TUnsafe */ +export function IsUnsafe(value: unknown): value is TUnsafe { + return IsKindOf(value, 'Unsafe') +} +/** Returns true if the given value is TVoid */ +export function IsVoid(value: unknown): value is TVoid { + // prettier-ignore + return ( + IsKindOf(value, 'Void') && + value.type === 'void' && + IsOptionalString(value.$id) + ) +} +/** Returns true if the given value is TKind */ +export function IsKind(value: unknown): value is Record & { [Kind]: string } { + return ValueGuard.IsObject(value) && Kind in value && ValueGuard.IsString(value[Kind]) && !KnownTypes.includes(value[Kind] as string) +} +/** Returns true if the given value is TSchema */ +export function IsSchema(value: unknown): value is TSchema { + // prettier-ignore + return ( + ValueGuard.IsObject(value) + ) && ( + IsAny(value) || + IsArgument(value) || + IsArray(value) || + IsBoolean(value) || + IsBigInt(value) || + IsAsyncIterator(value) || + IsComputed(value) || + IsConstructor(value) || + IsDate(value) || + IsFunction(value) || + IsInteger(value) || + IsIntersect(value) || + IsIterator(value) || + IsLiteral(value) || + IsMappedKey(value) || + IsMappedResult(value) || + IsNever(value) || + IsNot(value) || + IsNull(value) || + IsNumber(value) || + IsObject(value) || + IsPromise(value) || + IsRecord(value) || + IsRef(value) || + IsRegExp(value) || + IsString(value) || + IsSymbol(value) || + IsTemplateLiteral(value) || + IsThis(value) || + IsTuple(value) || + IsUndefined(value) || + IsUnion(value) || + IsUint8Array(value) || + IsUnknown(value) || + IsUnsafe(value) || + IsVoid(value) || + IsKind(value) + ) +} diff --git a/src/type/guard/value.ts b/src/type/guard/value.ts new file mode 100644 index 000000000..93ae57fe8 --- /dev/null +++ b/src/type/guard/value.ts @@ -0,0 +1,98 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +// -------------------------------------------------------------------------- +// PropertyKey +// -------------------------------------------------------------------------- +/** Returns true if this value has this property key */ +export function HasPropertyKey(value: Record, key: K): value is Record & { [_ in K]: unknown } { + return key in value +} +// -------------------------------------------------------------------------- +// Object Instances +// -------------------------------------------------------------------------- +/** Returns true if this value is an async iterator */ +export function IsAsyncIterator(value: unknown): value is AsyncIterableIterator { + return IsObject(value) && !IsArray(value) && !IsUint8Array(value) && Symbol.asyncIterator in value +} +/** Returns true if this value is an array */ +export function IsArray(value: unknown): value is unknown[] { + return Array.isArray(value) +} +/** Returns true if this value is bigint */ +export function IsBigInt(value: unknown): value is bigint { + return typeof value === 'bigint' +} +/** Returns true if this value is a boolean */ +export function IsBoolean(value: unknown): value is boolean { + return typeof value === 'boolean' +} +/** Returns true if this value is a Date object */ +export function IsDate(value: unknown): value is Date { + return value instanceof globalThis.Date +} +/** Returns true if this value is a function */ +export function IsFunction(value: unknown): value is Function { + return typeof value === 'function' +} +/** Returns true if this value is an iterator */ +export function IsIterator(value: unknown): value is IterableIterator { + return IsObject(value) && !IsArray(value) && !IsUint8Array(value) && Symbol.iterator in value +} +/** Returns true if this value is null */ +export function IsNull(value: unknown): value is null { + return value === null +} +/** Returns true if this value is number */ +export function IsNumber(value: unknown): value is number { + return typeof value === 'number' +} +/** Returns true if this value is an object */ +export function IsObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null +} +/** Returns true if this value is RegExp */ +export function IsRegExp(value: unknown): value is RegExp { + return value instanceof globalThis.RegExp +} +/** Returns true if this value is string */ +export function IsString(value: unknown): value is string { + return typeof value === 'string' +} +/** Returns true if this value is symbol */ +export function IsSymbol(value: unknown): value is symbol { + return typeof value === 'symbol' +} +/** Returns true if this value is a Uint8Array */ +export function IsUint8Array(value: unknown): value is Uint8Array { + return value instanceof globalThis.Uint8Array +} +/** Returns true if this value is undefined */ +export function IsUndefined(value: unknown): value is undefined { + return value === undefined +} diff --git a/src/type/helpers/helpers.ts b/src/type/helpers/helpers.ts new file mode 100644 index 000000000..a3cb86466 --- /dev/null +++ b/src/type/helpers/helpers.ts @@ -0,0 +1,69 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { TSchema } from '../schema/index' +import type { TProperties } from '../object/index' +import type { TNever } from '../never/index' +// ------------------------------------------------------------------ +// Helper: Common +// ------------------------------------------------------------------ +export type TupleToIntersect = T extends [infer I] ? I : T extends [infer I, ...infer R] ? I & TupleToIntersect : never +export type TupleToUnion = { [K in keyof T]: T[K] }[number] +export type UnionToIntersect = (U extends unknown ? (arg: U) => 0 : never) extends (arg: infer I) => 0 ? I : never +export type UnionLast = UnionToIntersect 0 : never> extends (x: infer L) => 0 ? L : never +export type UnionToTuple> = [U] extends [never] ? Acc : UnionToTuple, [Extract, ...Acc]> +export type Trim = T extends `${' '}${infer U}` ? Trim : T extends `${infer U}${' '}` ? Trim : T +export type Assert = T extends E ? T : never +export type Evaluate = T extends infer O ? { [K in keyof O]: O[K] } : never +export type Ensure = T extends infer U ? U : never +export type EmptyString = '' +export type ZeroString = '0' +// ------------------------------------------------------------------ +// Helper: Increment +// ------------------------------------------------------------------ +type IncrementBase = { m: '9'; t: '01'; '0': '1'; '1': '2'; '2': '3'; '3': '4'; '4': '5'; '5': '6'; '6': '7'; '7': '8'; '8': '9'; '9': '0' } +type IncrementTake = IncrementBase[T] +type IncrementStep = T extends IncrementBase['m'] + ? IncrementBase['t'] + : T extends `${infer L extends keyof IncrementBase}${infer R}` + ? L extends IncrementBase['m'] + ? `${IncrementTake}${IncrementStep}` + : `${IncrementTake}${R}` + : never +type IncrementReverse = T extends `${infer L}${infer R}` ? `${IncrementReverse}${L}` : T +export type TIncrement = IncrementReverse>> +/** Increments the given string value + 1 */ +export function Increment(T: T): TIncrement { + return (parseInt(T) + 1).toString() as never +} +// ------------------------------------------------------------------ +// Helper: Type Asserts +// ------------------------------------------------------------------ +export type AssertProperties = T extends TProperties ? T : TProperties +export type AssertRest = T extends E ? T : [] +export type AssertType = T extends E ? T : TNever diff --git a/src/type/helpers/index.ts b/src/type/helpers/index.ts new file mode 100644 index 000000000..31d3e5185 --- /dev/null +++ b/src/type/helpers/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './helpers' diff --git a/src/type/index.ts b/src/type/index.ts new file mode 100644 index 000000000..cafc2a7f6 --- /dev/null +++ b/src/type/index.ts @@ -0,0 +1,99 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './any/index' +export * from './argument/index' +export * from './array/index' +export * from './async-iterator/index' +export * from './awaited/index' +export * from './bigint/index' +export * from './boolean/index' +export * from './clone/index' +export * from './composite/index' +export * from './const/index' +export * from './constructor/index' +export * from './constructor-parameters/index' +export * from './date/index' +export * from './discard/index' +export * from './enum/index' +export * from './error/index' +export * from './exclude/index' +export * from './extends/index' +export * from './extract/index' +export * from './function/index' +export * from './guard/index' +export * from './helpers/index' +export * from './indexed/index' +export * from './instance-type/index' +export * from './instantiate/index' +export * from './integer/index' +export * from './intersect/index' +export * from './intrinsic/index' +export * from './iterator/index' +export * from './keyof/index' +export * from './literal/index' +export * from './mapped/index' +export * from './module/index' +export * from './never/index' +export * from './not/index' +export * from './null/index' +export * from './number/index' +export * from './object/index' +export * from './omit/index' +export * from './optional/index' +export * from './parameters/index' +export * from './partial/index' +export * from './patterns/index' +export * from './pick/index' +export * from './promise/index' +export * from './readonly/index' +export * from './readonly-optional/index' +export * from './record/index' +export * from './recursive/index' +export * from './ref/index' +export * from './regexp/index' +export * from './registry/index' +export * from './required/index' +export * from './rest/index' +export * from './return-type/index' +export * from './schema/index' +export * from './sets/index' +export * from './static/index' +export * from './string/index' +export * from './symbol/index' +export * from './symbols/index' +export * from './template-literal/index' +export * from './transform/index' +export * from './tuple/index' +export * from './type/index' +export * from './uint8array/index' +export * from './undefined/index' +export * from './union/index' +export * from './unknown/index' +export * from './unsafe/index' +export * from './void/index' diff --git a/src/type/indexed/index.ts b/src/type/indexed/index.ts new file mode 100644 index 000000000..b13a8c9b5 --- /dev/null +++ b/src/type/indexed/index.ts @@ -0,0 +1,32 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './indexed-from-mapped-key' +export * from './indexed-from-mapped-result' +export * from './indexed-property-keys' +export * from './indexed' diff --git a/src/type/indexed/indexed-from-mapped-key.ts b/src/type/indexed/indexed-from-mapped-key.ts new file mode 100644 index 000000000..ae99317eb --- /dev/null +++ b/src/type/indexed/indexed-from-mapped-key.ts @@ -0,0 +1,92 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { TSchema, SchemaOptions } from '../schema/index' +import type { Ensure, Evaluate } from '../helpers/index' +import type { TProperties } from '../object/index' +import { Index, type TIndex } from './indexed' +import { MappedResult, type TMappedResult, type TMappedKey } from '../mapped/index' +import { Clone } from '../clone/value' + +// ------------------------------------------------------------------ +// MappedIndexPropertyKey +// ------------------------------------------------------------------ +// prettier-ignore +type TMappedIndexPropertyKey = { + [_ in Key]: TIndex +} +// prettier-ignore +function MappedIndexPropertyKey(type: Type, key: Key, options?: SchemaOptions): TMappedIndexPropertyKey { + return { [key]: Index(type, [key], Clone(options)) } as never +} +// ------------------------------------------------------------------ +// MappedIndexPropertyKeys +// ------------------------------------------------------------------ +// prettier-ignore +type TMappedIndexPropertyKeys = ( + PropertyKeys extends [infer Left extends PropertyKey, ...infer Right extends PropertyKey[]] + ? TMappedIndexPropertyKeys> + : Result +) +// prettier-ignore +function MappedIndexPropertyKeys< + Type extends TSchema, + PropertyKeys extends PropertyKey[] +>(type: Type, propertyKeys: [...PropertyKeys], options?: SchemaOptions): TMappedIndexPropertyKeys { + return propertyKeys.reduce((result, left) => { + return { ...result, ...MappedIndexPropertyKey(type, left, options) } + }, {} as TProperties) as never +} +// ------------------------------------------------------------------ +// MappedIndexProperties +// ------------------------------------------------------------------ +// prettier-ignore +type TMappedIndexProperties = Evaluate< + TMappedIndexPropertyKeys +> +// prettier-ignore +function MappedIndexProperties(type: Type, mappedKey: MappedKey, options?: SchemaOptions): TMappedIndexProperties { + return MappedIndexPropertyKeys(type, mappedKey.keys, options) as never +} +// ------------------------------------------------------------------ +// TIndexFromMappedKey +// ------------------------------------------------------------------ +// prettier-ignore +export type TIndexFromMappedKey +> = ( + Ensure> +) +// prettier-ignore +export function IndexFromMappedKey +>(type: Type, mappedKey: MappedKey, options?: SchemaOptions): TMappedResult { + const properties = MappedIndexProperties(type, mappedKey, options) + return MappedResult(properties) as never +} diff --git a/src/type/indexed/indexed-from-mapped-result.ts b/src/type/indexed/indexed-from-mapped-result.ts new file mode 100644 index 000000000..b5b3d67ca --- /dev/null +++ b/src/type/indexed/indexed-from-mapped-result.ts @@ -0,0 +1,76 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { TSchema, SchemaOptions } from '../schema/index' +import type { TProperties } from '../object/index' +import { MappedResult, type TMappedResult } from '../mapped/index' +import { IndexPropertyKeys, type TIndexPropertyKeys } from './indexed-property-keys' +import { Index, type TIndex } from './index' + +// ------------------------------------------------------------------ +// FromProperties +// ------------------------------------------------------------------ +// prettier-ignore +type TFromProperties = ( + { [K2 in keyof Properties]: TIndex> } +) +// prettier-ignore +function FromProperties(type: Type, properties: Properties, options?: SchemaOptions): TFromProperties { + const result = {} as Record + for(const K2 of Object.getOwnPropertyNames(properties)) { + result[K2] = Index(type, IndexPropertyKeys(properties[K2]), options) + } + return result as never +} +// ------------------------------------------------------------------ +// FromMappedResult +// ------------------------------------------------------------------ +// prettier-ignore +type TFromMappedResult = ( + TFromProperties +) +// prettier-ignore +function FromMappedResult(type: Type, mappedResult: MappedResult, options?: SchemaOptions): TFromMappedResult { + return FromProperties(type, mappedResult.properties, options) as never +} +// ------------------------------------------------------------------ +// TIndexFromMappedResult +// ------------------------------------------------------------------ +// prettier-ignore +export type TIndexFromMappedResult +> = ( + TMappedResult +) +// prettier-ignore +export function IndexFromMappedResult +>(type: Type, mappedResult: MappedResult, options?: SchemaOptions): TMappedResult { + const properties = FromMappedResult(type, mappedResult, options) + return MappedResult(properties) as never +} diff --git a/src/type/indexed/indexed-property-keys.ts b/src/type/indexed/indexed-property-keys.ts new file mode 100644 index 000000000..e04866414 --- /dev/null +++ b/src/type/indexed/indexed-property-keys.ts @@ -0,0 +1,103 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { TemplateLiteralGenerate, type TTemplateLiteralGenerate, type TTemplateLiteral } from '../template-literal/index' +import type { TLiteral, TLiteralValue } from '../literal/index' +import type { TInteger } from '../integer/index' +import type { TNumber } from '../number/index' +import type { TSchema } from '../schema/index' +import type { TUnion } from '../union/index' + +// ------------------------------------------------------------------ +// TypeGuard +// ------------------------------------------------------------------ +import { IsTemplateLiteral, IsUnion, IsLiteral, IsNumber, IsInteger } from '../guard/kind' +// ------------------------------------------------------------------ +// FromTemplateLiteral +// ------------------------------------------------------------------ +// prettier-ignore +type TFromTemplateLiteral> = (Keys) +// prettier-ignore +function FromTemplateLiteral(templateLiteral: TemplateLiteral): TFromTemplateLiteral { + const keys = TemplateLiteralGenerate(templateLiteral) as string[] + return keys.map(key => key.toString()) as never +} +// ------------------------------------------------------------------ +// FromUnion +// ------------------------------------------------------------------ +// prettier-ignore +type TFromUnion = ( + Types extends [infer Left extends TSchema, ...infer Right extends TSchema[]] + ? TFromUnion]> + : Result +) +// prettier-ignore +function FromUnion(types: Types): TFromUnion { + const result = [] as string[] + for(const type of types) result.push(...IndexPropertyKeys(type)) + return result as never +} +// ------------------------------------------------------------------ +// FromLiteral +// ------------------------------------------------------------------ +// prettier-ignore +type TFromLiteral = ( + LiteralValue extends PropertyKey + ? [`${LiteralValue}`] + : [] +) +// prettier-ignore +function FromLiteral(literalValue: LiteralValue): TFromLiteral { + return ( + [(literalValue as string).toString()] // TS 5.4 observes TLiteralValue as not having a toString() + ) as never +} +// ------------------------------------------------------------------ +// IndexPropertyKeys +// ------------------------------------------------------------------ +// prettier-ignore +export type TIndexPropertyKeys = ( + Type extends TTemplateLiteral ? TFromTemplateLiteral : + Type extends TUnion ? TFromUnion : + Type extends TLiteral ? TFromLiteral : + Type extends TNumber ? ['[number]'] : + Type extends TInteger ? ['[number]'] : + [] +) +/** Returns a tuple of PropertyKeys derived from the given TSchema */ +// prettier-ignore +export function IndexPropertyKeys(type: Type): TIndexPropertyKeys { + return [...new Set(( + IsTemplateLiteral(type) ? FromTemplateLiteral(type) : + IsUnion(type) ? FromUnion(type.anyOf) : + IsLiteral(type) ? FromLiteral(type.const) : + IsNumber(type) ? ['[number]'] : + IsInteger(type) ? ['[number]'] : + [] + ))] as never +} diff --git a/src/type/indexed/indexed.ts b/src/type/indexed/indexed.ts new file mode 100644 index 000000000..1e6603bf5 --- /dev/null +++ b/src/type/indexed/indexed.ts @@ -0,0 +1,300 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { CreateType } from '../create/type' +import { TypeBoxError } from '../error/index' + +import { type TSchema, SchemaOptions } from '../schema/index' +import { type Assert } from '../helpers/index' +import { type TComputed, Computed } from '../computed/index' +import { type TNever, Never } from '../never/index' +import { type TArray } from '../array/index' +import { type TIntersect } from '../intersect/index' +import { type TMappedResult, type TMappedKey } from '../mapped/index' +import { type TObject, type TProperties } from '../object/index' +import { type TUnion } from '../union/index' +import { type TRecursive } from '../recursive/index' +import { type TRef } from '../ref/index' +import { type TTuple } from '../tuple/index' + +import { IntersectEvaluated, type TIntersectEvaluated } from '../intersect/index' +import { UnionEvaluated, type TUnionEvaluated } from '../union/index' + +import { IndexPropertyKeys, type TIndexPropertyKeys } from './indexed-property-keys' +import { IndexFromMappedKey, type TIndexFromMappedKey } from './indexed-from-mapped-key' +import { IndexFromMappedResult, type TIndexFromMappedResult } from './indexed-from-mapped-result' + +// ------------------------------------------------------------------ +// TypeGuard +// ------------------------------------------------------------------ +import { IsArray, IsIntersect, IsObject, IsMappedKey, IsMappedResult, IsNever, IsSchema, IsTuple, IsUnion, IsRef } from '../guard/kind' + +// ------------------------------------------------------------------ +// FromRest +// ------------------------------------------------------------------ +// prettier-ignore +type TFromRest = ( + Types extends [infer Left extends TSchema, ...infer Right extends TSchema[]] + ? TFromRest, TSchema>]> + : Result +) +// prettier-ignore +function FromRest(types: [...Types], key: Key): TFromRest { + return types.map(type => IndexFromPropertyKey(type, key)) as never +} +// ------------------------------------------------------------------ +// FromIntersectRest +// ------------------------------------------------------------------ +// prettier-ignore +type TFromIntersectRest = ( + Types extends [infer Left extends TSchema, ...infer Right extends TSchema[]] + ? Left extends TNever + ? TFromIntersectRest + : TFromIntersectRest + : Result +) +// prettier-ignore +function FromIntersectRest(types: [...Types]): TFromIntersectRest { + return types.filter(type => !IsNever(type)) as never +} +// prettier-ignore +type TFromIntersect = ( + TIntersectEvaluated>> +) +// prettier-ignore +function FromIntersect(types: [...Types], key: Key): TFromIntersect { + return ( + IntersectEvaluated(FromIntersectRest(FromRest(types as TSchema[], key))) + ) as never +} +// ------------------------------------------------------------------ +// FromUnionRest +// +// The following accept a tuple of indexed key results. When evaluating +// these results, we check if any result evaluated to TNever. For key +// indexed unions, a TNever result indicates that the key was not +// present on the variant. In these cases, we must evaluate the indexed +// union to TNever (as given by a [] result). This logic aligns to the +// following behaviour. +// +// Non-Overlapping Union +// +// type A = { a: string } +// type B = { b: string } +// type C = (A | B) & { a: number } // C is { a: number } +// +// Overlapping Union +// +// type A = { a: string } +// type B = { a: string } +// type C = (A | B) & { a: number } // C is { a: never } +// +// ------------------------------------------------------------------ +// prettier-ignore +type TFromUnionRest = + Types extends [infer Left extends TSchema, ...infer Right extends TSchema[]] + ? Left extends TNever + ? [] + : TFromUnionRest + : Result +// prettier-ignore +function FromUnionRest(types: [...Types]): TFromUnionRest { + return ( + types.some(L => IsNever(L)) + ? [] + : types + ) as never +} +// ------------------------------------------------------------------ +// FromUnion +// ------------------------------------------------------------------ +// prettier-ignore +type TFromUnion = ( + TUnionEvaluated>> +) +// prettier-ignore +function FromUnion(types: [...Types], key: Key): TFromUnion { + return ( + UnionEvaluated(FromUnionRest(FromRest(types as TSchema[], key))) + ) as never +} +// ------------------------------------------------------------------ +// FromTuple +// ------------------------------------------------------------------ +// prettier-ignore +type TFromTuple = ( + Key extends keyof Types ? Types[Key] : + Key extends '[number]' ? TUnionEvaluated : + TNever +) +// prettier-ignore +function FromTuple(types: [...Types], key: Key): TFromTuple { + return ( + key in types ? types[key as number] : + key === '[number]' ? UnionEvaluated(types) : + Never() + ) as never +} +// ------------------------------------------------------------------ +// FromArray +// ------------------------------------------------------------------ +// prettier-ignore +type TFromArray = ( + Key extends '[number]' + ? Type + : TNever +) +// prettier-ignore +function FromArray(type: Type, key: Key): TFromArray { + return ( + key === '[number]' + ? type + : Never() + ) as never +} +// ------------------------------------------------------------------ +// FromProperty +// ------------------------------------------------------------------ +type AssertPropertyKey = Assert + +// prettier-ignore +type TFromProperty = ( + // evaluate for string keys + Key extends keyof Properties + ? Properties[Key] + // evaluate for numeric keys + : `${AssertPropertyKey}` extends `${AssertPropertyKey}` + ? Properties[AssertPropertyKey] + : TNever +) +// prettier-ignore +function FromProperty(properties: Properties, propertyKey: Key): TFromProperty { + return (propertyKey in properties ? properties[propertyKey as string] : Never()) as never +} +// ------------------------------------------------------------------ +// FromKey +// ------------------------------------------------------------------ +// prettier-ignore +export type TIndexFromPropertyKey = ( + Type extends TRecursive ? TIndexFromPropertyKey : + Type extends TIntersect ? TFromIntersect : + Type extends TUnion ? TFromUnion : + Type extends TTuple ? TFromTuple : + Type extends TArray ? TFromArray : + Type extends TObject ? TFromProperty : + TNever +) +// prettier-ignore +export function IndexFromPropertyKey(type: Type, propertyKey: Key): TIndexFromPropertyKey { + return ( + IsIntersect(type) ? FromIntersect(type.allOf, propertyKey) : + IsUnion(type) ? FromUnion(type.anyOf, propertyKey) : + IsTuple(type) ? FromTuple(type.items ?? [], propertyKey) : + IsArray(type) ? FromArray(type.items, propertyKey) : + IsObject(type) ? FromProperty(type.properties, propertyKey) : + Never() + ) as never +} +// ------------------------------------------------------------------ +// FromKeys +// ------------------------------------------------------------------ +// prettier-ignore +export type TIndexFromPropertyKeys = ( + PropertyKeys extends [infer Left extends PropertyKey, ...infer Right extends PropertyKey[]] + ? TIndexFromPropertyKeys, TSchema>]> + : Result +) +// prettier-ignore +export function IndexFromPropertyKeys(type: Type, propertyKeys: [...PropertyKeys]): TIndexFromPropertyKeys { + return propertyKeys.map(propertyKey => IndexFromPropertyKey(type, propertyKey)) as never +} +// ------------------------------------------------------------------ +// FromSchema +// ------------------------------------------------------------------ +// prettier-ignore +type FromSchema = ( + TUnionEvaluated> +) +// prettier-ignore +function FromSchema(type: Type, propertyKeys: [...PropertyKeys]): FromSchema { + return ( + UnionEvaluated(IndexFromPropertyKeys(type, propertyKeys as PropertyKey[])) + ) as never +} +// ------------------------------------------------------------------ +// FromSchema +// ------------------------------------------------------------------ +// prettier-ignore +export type TIndexFromComputed = ( + TComputed<'Index', [Type, Key]> +) +// prettier-ignore +export function IndexFromComputed(type: Type, key: Key): TIndexFromComputed { + return Computed('Index', [type, key]) +} +// ------------------------------------------------------------------ +// TIndex +// ------------------------------------------------------------------ +// prettier-ignore +export type TIndex = ( + FromSchema +) +/** `[Json]` Returns an Indexed property type for the given keys */ +export function Index(type: Type, key: Key, options?: SchemaOptions): TIndexFromComputed +/** `[Json]` Returns an Indexed property type for the given keys */ +export function Index(type: Type, key: Key, options?: SchemaOptions): TIndexFromComputed +/** `[Json]` Returns an Indexed property type for the given keys */ +export function Index(type: Type, key: Key, options?: SchemaOptions): TIndexFromComputed +/** `[Json]` Returns an Indexed property type for the given keys */ +export function Index(type: Type, mappedResult: MappedResult, options?: SchemaOptions): TIndexFromMappedResult +/** `[Json]` Returns an Indexed property type for the given keys */ +export function Index(type: Type, mappedResult: MappedResult, options?: SchemaOptions): TIndexFromMappedResult +/** `[Json]` Returns an Indexed property type for the given keys */ +export function Index(type: Type, mappedKey: MappedKey, options?: SchemaOptions): TIndexFromMappedKey +/** `[Json]` Returns an Indexed property type for the given keys */ +export function Index>(T: Type, K: Key, options?: SchemaOptions): TIndex +/** `[Json]` Returns an Indexed property type for the given keys */ +export function Index(type: Type, propertyKeys: readonly [...PropertyKeys], options?: SchemaOptions): TIndex +/** `[Json]` Returns an Indexed property type for the given keys */ +export function Index(type: TSchema, key: any, options?: SchemaOptions): any { + // computed-type + if (IsRef(type) || IsRef(key)) { + const error = `Index types using Ref parameters require both Type and Key to be of TSchema` + if (!IsSchema(type) || !IsSchema(key)) throw new TypeBoxError(error) + return Computed('Index', [type, key]) + } + // mapped-types + if (IsMappedResult(key)) return IndexFromMappedResult(type, key, options) + if (IsMappedKey(key)) return IndexFromMappedKey(type, key, options) + // prettier-ignore + return CreateType( + IsSchema(key) + ? FromSchema(type, IndexPropertyKeys(key)) + : FromSchema(type, key as string[]) + , options) as never +} diff --git a/src/type/instance-type/index.ts b/src/type/instance-type/index.ts new file mode 100644 index 000000000..35a3a954d --- /dev/null +++ b/src/type/instance-type/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './instance-type' diff --git a/src/type/instance-type/instance-type.ts b/src/type/instance-type/instance-type.ts new file mode 100644 index 000000000..7bba6663c --- /dev/null +++ b/src/type/instance-type/instance-type.ts @@ -0,0 +1,45 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { CreateType } from '../create/type' +import { type TSchema, SchemaOptions } from '../schema/index' +import { type TConstructor } from '../constructor/index' +import { type TNever, Never } from '../never/index' +import * as KindGuard from '../guard/kind' + +// prettier-ignore +export type TInstanceType + ? InstanceType + : TNever +> = Result + +/** `[JavaScript]` Extracts the InstanceType from the given Constructor type */ +export function InstanceType(schema: Type, options?: SchemaOptions): TInstanceType { + return (KindGuard.IsConstructor(schema) ? CreateType(schema.returns, options) : Never(options)) as never +} diff --git a/src/type/instantiate/index.ts b/src/type/instantiate/index.ts new file mode 100644 index 000000000..5f0a519b0 --- /dev/null +++ b/src/type/instantiate/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './instantiate' diff --git a/src/type/instantiate/instantiate.ts b/src/type/instantiate/instantiate.ts new file mode 100644 index 000000000..3cb2b4857 --- /dev/null +++ b/src/type/instantiate/instantiate.ts @@ -0,0 +1,307 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { CloneType } from '../clone/type' +import { type TSchema } from '../schema/index' +import { type TArgument } from '../argument/index' +import { type TUnknown, Unknown } from '../unknown/index' +import { type TReadonlyOptional, ReadonlyOptional } from '../readonly-optional/index' +import { type TReadonly, Readonly } from '../readonly/index' +import { type TOptional, Optional } from '../optional/index' +import { type TConstructor } from '../constructor/index' +import { type TFunction } from '../function/index' +import { type TIntersect } from '../intersect/index' +import { type TUnion } from '../union/index' +import { type TTuple } from '../tuple/index' +import { type TArray } from '../array/index' +import { type TAsyncIterator } from '../async-iterator/index' +import { type TIterator } from '../iterator/index' +import { type TPromise } from '../promise/index' +import { type TObject, type TProperties, Object } from '../object/index' +import { type TRecordOrObject, type TRecord, Record, RecordKey, RecordValue } from '../record/index' + +import * as ValueGuard from '../guard/value' +import * as KindGuard from '../guard/kind' + +// ------------------------------------------------------------------ +// Constructor +// ------------------------------------------------------------------ +// prettier-ignore +type TFromConstructor, TFromType> +> = Result +// prettier-ignore +function FromConstructor(args: TSchema[], type: TConstructor): TConstructor { + type.parameters = FromTypes(args, type.parameters) + type.returns = FromType(args, type.returns) + return type +} +// ------------------------------------------------------------------ +// Function +// ------------------------------------------------------------------ +// prettier-ignore +type TFromFunction, TFromType> +> = Result +// prettier-ignore +function FromFunction(args: TSchema[], type: TFunction): TFunction { + type.parameters = FromTypes(args, type.parameters) + type.returns = FromType(args, type.returns) + return type +} +// ------------------------------------------------------------------ +// Intersect +// ------------------------------------------------------------------ +// prettier-ignore +type TFromIntersect> +> = Result +// prettier-ignore +function FromIntersect(args: TSchema[], type: TIntersect): TIntersect { + type.allOf = FromTypes(args, type.allOf) + return type +} +// ------------------------------------------------------------------ +// Union +// ------------------------------------------------------------------ +// prettier-ignore +type TFromUnion> +> = Result +// prettier-ignore +function FromUnion(args: TSchema[], type: TUnion): TUnion { + type.anyOf = FromTypes(args, type.anyOf) + return type +} +// ------------------------------------------------------------------ +// Tuple +// ------------------------------------------------------------------ +// prettier-ignore +type TFromTuple> +> = Result +// prettier-ignore +function FromTuple(args: TSchema[], type: TTuple): TTuple { + if(ValueGuard.IsUndefined(type.items)) return type + type.items = FromTypes(args, type.items!) + return type +} +// ------------------------------------------------------------------ +// Array +// ------------------------------------------------------------------ +// prettier-ignore +type TFromArray> +> = Result +// prettier-ignore +function FromArray(args: TSchema[], type: TArray): TArray { + type.items = FromType(args, type.items) + return type +} +// ------------------------------------------------------------------ +// AsyncIterator +// ------------------------------------------------------------------ +// prettier-ignore +type TFromAsyncIterator> +> = Result +// prettier-ignore +function FromAsyncIterator(args: TSchema[], type: TAsyncIterator): TAsyncIterator { + type.items = FromType(args, type.items) + return type +} +// ------------------------------------------------------------------ +// Iterator +// ------------------------------------------------------------------ +// prettier-ignore +type TFromIterator> +> = Result +// prettier-ignore +function FromIterator(args: TSchema[], type: TIterator): TIterator { + type.items = FromType(args, type.items) + return type +} +// ------------------------------------------------------------------ +// Promise +// ------------------------------------------------------------------ +// prettier-ignore +type TFromPromise> +> = Result +// prettier-ignore +function FromPromise(args: TSchema[], type: TPromise): TPromise { + type.item = FromType(args, type.item) + return type +} +// ------------------------------------------------------------------ +// Object +// ------------------------------------------------------------------ +// prettier-ignore +type TFromObject, + Result extends TObject = TObject +> = Result +// prettier-ignore +function FromObject(args: TSchema[], type: TObject): TObject { + const mappedProperties = FromProperties(args, type.properties) + return { ...type, ...Object(mappedProperties) } // retain options +} +// ------------------------------------------------------------------ +// Object +// ------------------------------------------------------------------ +// prettier-ignore +type TFromRecord, + MappedValue extends TSchema = TFromType, + Result extends TSchema = TRecordOrObject +> = Result +// prettier-ignore +function FromRecord(args: TSchema[], type: TRecord): TRecord { + const mappedKey = FromType(args, RecordKey(type)) + const mappedValue = FromType(args, RecordValue(type)) + const result = Record(mappedKey, mappedValue) + return { ...type, ... result } as never // retain options +} +// ------------------------------------------------------------------ +// Argument +// ------------------------------------------------------------------ +// prettier-ignore +type TFromArgument = Result +// prettier-ignore +function FromArgument(args: TSchema[], argument: TArgument): TSchema { + return argument.index in args ? args[argument.index] : Unknown() +} +// ------------------------------------------------------------------ +// Property +// ------------------------------------------------------------------ +// prettier-ignore +type TFromProperty ? true : false, + IsOptional extends boolean = Type extends TOptional ? true : false, + Mapped extends TSchema = TFromType, + Result extends TSchema = ( + [IsReadonly, IsOptional] extends [true, true] ? TReadonlyOptional : + [IsReadonly, IsOptional] extends [true, false] ? TReadonly : + [IsReadonly, IsOptional] extends [false, true] ? TOptional : + Mapped + ) +> = Result +// prettier-ignore +function FromProperty(args: [...Args], type: Type): TFromProperty { + const isReadonly = KindGuard.IsReadonly(type) + const isOptional = KindGuard.IsOptional(type) + const mapped = FromType(args, type) + return ( + isReadonly && isOptional ? ReadonlyOptional(mapped) : + isReadonly && !isOptional ? Readonly(mapped) : + !isReadonly && isOptional ? Optional(mapped) : + mapped + ) as never +} +// ------------------------------------------------------------------ +// Properties +// ------------------------------------------------------------------ +// prettier-ignore +type TFromProperties + } +> = Result +// prettier-ignore +function FromProperties(args: TSchema[], properties: TProperties): TFromProperties { + return globalThis.Object.getOwnPropertyNames(properties).reduce((result, key) => { + return { ...result, [key]: FromProperty(args, properties[key]) } + }, {}) as never +} +// ------------------------------------------------------------------ +// Types +// ------------------------------------------------------------------ +// prettier-ignore +export type TFromTypes = ( + Types extends [infer Left extends TSchema, ...infer Right extends TSchema[]] + ? TFromTypes]> + : Result +) +// prettier-ignore +export function FromTypes(args: [...Args], types: [...Types]): TFromTypes { + return types.map(type => FromType(args, type)) as never +} +// ------------------------------------------------------------------ +// Type +// ------------------------------------------------------------------ +// prettier-ignore +export type TFromType = ( + Type extends TConstructor ? TFromConstructor : + Type extends TFunction ? TFromFunction : + Type extends TIntersect ? TFromIntersect : + Type extends TUnion ? TFromUnion : + Type extends TTuple ? TFromTuple : + Type extends TArray ? TFromArray: + Type extends TAsyncIterator ? TFromAsyncIterator : + Type extends TIterator ? TFromIterator : + Type extends TPromise ? TFromPromise : + Type extends TObject ? TFromObject : + Type extends TRecord ? TFromRecord : + Type extends TArgument ? TFromArgument : + Type +) +// prettier-ignore +function FromType(args: [...Args], type: TSchema): TFromType { + return ( + KindGuard.IsConstructor(type) ? FromConstructor(args, type) : + KindGuard.IsFunction(type) ? FromFunction(args, type) : + KindGuard.IsIntersect(type) ? FromIntersect(args, type) : + KindGuard.IsUnion(type) ? FromUnion(args, type) : + KindGuard.IsTuple(type) ? FromTuple(args, type) : + KindGuard.IsArray(type) ? FromArray(args, type) : + KindGuard.IsAsyncIterator(type) ? FromAsyncIterator(args, type) : + KindGuard.IsIterator(type) ? FromIterator(args, type) : + KindGuard.IsPromise(type) ? FromPromise(args, type) : + KindGuard.IsObject(type) ? FromObject(args, type): + KindGuard.IsRecord(type) ? FromRecord(args, type) : + KindGuard.IsArgument(type) ? FromArgument(args, type) : + type + ) as never +} +// ------------------------------------------------------------------ +// Instantiate +// ------------------------------------------------------------------ +/** `[JavaScript]` Instantiates a type with the given parameters */ +// prettier-ignore +export type TInstantiate +> = Result + +/** `[JavaScript]` Instantiates a type with the given parameters */ +// prettier-ignore +export function Instantiate(type: Type, args: [...Args]): TInstantiate { + return FromType(args, CloneType(type)) +} diff --git a/src/type/integer/index.ts b/src/type/integer/index.ts new file mode 100644 index 000000000..8c78b4721 --- /dev/null +++ b/src/type/integer/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './integer' diff --git a/src/type/integer/integer.ts b/src/type/integer/integer.ts new file mode 100644 index 000000000..245037e1e --- /dev/null +++ b/src/type/integer/integer.ts @@ -0,0 +1,48 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { CreateType } from '../create/type' +import type { TSchema, SchemaOptions } from '../schema/index' +import { Kind } from '../symbols/index' + +export interface IntegerOptions extends SchemaOptions { + exclusiveMaximum?: number + exclusiveMinimum?: number + maximum?: number + minimum?: number + multipleOf?: number +} +export interface TInteger extends TSchema, IntegerOptions { + [Kind]: 'Integer' + static: number + type: 'integer' +} +/** `[Json]` Creates an Integer type */ +export function Integer(options?: IntegerOptions): TInteger { + return CreateType({ [Kind]: 'Integer', type: 'integer' }, options) as never +} diff --git a/src/type/intersect/index.ts b/src/type/intersect/index.ts new file mode 100644 index 000000000..67a0a8911 --- /dev/null +++ b/src/type/intersect/index.ts @@ -0,0 +1,31 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './intersect-evaluated' +export * from './intersect-type' +export * from './intersect' diff --git a/src/type/intersect/intersect-create.ts b/src/type/intersect/intersect-create.ts new file mode 100644 index 000000000..59f6cf64e --- /dev/null +++ b/src/type/intersect/intersect-create.ts @@ -0,0 +1,52 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { CreateType } from '../create/type' +import type { TSchema } from '../schema/index' +import { Kind } from '../symbols/index' +import type { TIntersect, IntersectOptions } from './intersect-type' + +// ------------------------------------------------------------------ +// TypeGuard +// ------------------------------------------------------------------ +import { IsObject, IsSchema } from '../guard/kind' +// ------------------------------------------------------------------ +// IntersectCreate +// ------------------------------------------------------------------ +// prettier-ignore +export function IntersectCreate(T: [...T], options: IntersectOptions = {}): TIntersect { + const allObjects = T.every((schema) => IsObject(schema)) + const clonedUnevaluatedProperties = IsSchema(options.unevaluatedProperties) + ? { unevaluatedProperties: options.unevaluatedProperties } + : {} + return CreateType( + (options.unevaluatedProperties === false || IsSchema(options.unevaluatedProperties) || allObjects + ? { ...clonedUnevaluatedProperties, [Kind]: 'Intersect', type: 'object', allOf: T } + : { ...clonedUnevaluatedProperties, [Kind]: 'Intersect', allOf: T }) + , options) as never +} diff --git a/src/type/intersect/intersect-evaluated.ts b/src/type/intersect/intersect-evaluated.ts new file mode 100644 index 000000000..2ac4a3c0d --- /dev/null +++ b/src/type/intersect/intersect-evaluated.ts @@ -0,0 +1,122 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { SchemaOptions, TSchema } from '../schema/index' +import { OptionalKind } from '../symbols/index' +import { CreateType } from '../create/type' +import { Discard } from '../discard/index' +import { Never, type TNever } from '../never/index' +import { Optional, type TOptional } from '../optional/index' +import type { TReadonly } from '../readonly/index' + +import { TIntersect, IntersectOptions } from './intersect-type' +import { IntersectCreate } from './intersect-create' + +// ------------------------------------------------------------------ +// TypeGuard +// ------------------------------------------------------------------ +import { IsOptional, IsTransform } from '../guard/kind' + +// ------------------------------------------------------------------ +// IsIntersectOptional +// ------------------------------------------------------------------ +// prettier-ignore +type TIsIntersectOptional = ( + Types extends [infer Left extends TSchema, ...infer Right extends TSchema[]] + ? Left extends TOptional + ? TIsIntersectOptional + : false + : true +) +// prettier-ignore +function IsIntersectOptional(types: [...Types]): TIsIntersectOptional { + return types.every(left => IsOptional(left)) as never +} +// ------------------------------------------------------------------ +// RemoveOptionalFromType +// ------------------------------------------------------------------ +// prettier-ignore +type TRemoveOptionalFromType = ( + Type extends TReadonly ? TReadonly> : + Type extends TOptional ? TRemoveOptionalFromType : + Type +) +// prettier-ignore +function RemoveOptionalFromType(type: Type): TRemoveOptionalFromType { + return ( + Discard(type, [OptionalKind]) + ) as never +} +// ------------------------------------------------------------------ +// RemoveOptionalFromRest +// ------------------------------------------------------------------ +// prettier-ignore +type TRemoveOptionalFromRest = ( + Types extends [infer Left extends TSchema, ...infer Right extends TSchema[]] + ? Left extends TOptional + ? TRemoveOptionalFromRest]> + : TRemoveOptionalFromRest + : Result +) +// prettier-ignore +function RemoveOptionalFromRest(types: [...Types]): TRemoveOptionalFromRest { + return types.map(left => IsOptional(left) ? RemoveOptionalFromType(left) : left) as never +} +// ------------------------------------------------------------------ +// ResolveIntersect +// ------------------------------------------------------------------ +// prettier-ignore +type TResolveIntersect = ( + TIsIntersectOptional extends true + ? TOptional>> + : TIntersect> +) +// prettier-ignore +function ResolveIntersect(types: [...Types], options: SchemaOptions): TResolveIntersect { + return ( + IsIntersectOptional(types) + ? Optional(IntersectCreate(RemoveOptionalFromRest(types) as TSchema[], options)) + : IntersectCreate(RemoveOptionalFromRest(types) as TSchema[], options) + ) as never +} +// ------------------------------------------------------------------ +// IntersectEvaluated +// ------------------------------------------------------------------ +// prettier-ignore +export type TIntersectEvaluated = ( + Types extends [TSchema] ? Types[0] : + Types extends [] ? TNever : + TResolveIntersect +) +/** `[Json]` Creates an evaluated Intersect type */ +export function IntersectEvaluated>(types: [...Types], options: IntersectOptions = {}): Result { + if (types.length === 1) return CreateType(types[0], options) as never + if (types.length === 0) return Never(options) as never + if (types.some((schema) => IsTransform(schema))) throw new Error('Cannot intersect transform types') + return ResolveIntersect(types, options) as never +} diff --git a/src/type/intersect/intersect-type.ts b/src/type/intersect/intersect-type.ts new file mode 100644 index 000000000..80b2c25e5 --- /dev/null +++ b/src/type/intersect/intersect-type.ts @@ -0,0 +1,56 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { TSchema, SchemaOptions } from '../schema/index' +import type { Static } from '../static/index' +import { Kind } from '../symbols/index' + +// ------------------------------------------------------------------ +// IntersectStatic +// ------------------------------------------------------------------ +// prettier-ignore +type TIntersectStatic = + T extends [infer L extends TSchema, ...infer R extends TSchema[]] + ? TIntersectStatic> + : Acc +// ------------------------------------------------------------------ +// TIntersect +// ------------------------------------------------------------------ +// prettier-ignore +export type TUnevaluatedProperties = undefined | TSchema | boolean +// prettier-ignore +export interface IntersectOptions extends SchemaOptions { + unevaluatedProperties?: TUnevaluatedProperties +} +// prettier-ignore +export interface TIntersect extends TSchema, IntersectOptions { + [Kind]: 'Intersect' + static: TIntersectStatic + type?: 'object' + allOf: [...T] +} diff --git a/src/type/intersect/intersect.ts b/src/type/intersect/intersect.ts new file mode 100644 index 000000000..63dd76b09 --- /dev/null +++ b/src/type/intersect/intersect.ts @@ -0,0 +1,54 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { CreateType } from '../create/type' +import type { TSchema } from '../schema/index' +import { Never, type TNever } from '../never/index' +import { TIntersect, IntersectOptions } from './intersect-type' +import { IntersectCreate } from './intersect-create' + +// ------------------------------------------------------------------ +// TypeGuard +// ------------------------------------------------------------------ +import { IsTransform } from '../guard/kind' +// ------------------------------------------------------------------ +// Intersect +// ------------------------------------------------------------------ +// prettier-ignore +export type Intersect = ( + Types extends [TSchema] ? Types[0] : + Types extends [] ? TNever : + TIntersect +) +/** `[Json]` Creates an evaluated Intersect type */ +export function Intersect(types: [...Types], options?: IntersectOptions): Intersect { + if (types.length === 1) return CreateType(types[0], options) as never + if (types.length === 0) return Never(options) as never + if (types.some((schema) => IsTransform(schema))) throw new Error('Cannot intersect transform types') + return IntersectCreate(types, options) as never +} diff --git a/src/type/intrinsic/capitalize.ts b/src/type/intrinsic/capitalize.ts new file mode 100644 index 000000000..51309eac6 --- /dev/null +++ b/src/type/intrinsic/capitalize.ts @@ -0,0 +1,37 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { TSchema, SchemaOptions } from '../schema/index' +import { Intrinsic, type TIntrinsic } from './intrinsic' + +// prettier-ignore +export type TCapitalize = TIntrinsic +/** `[Json]` Intrinsic function to Capitalize LiteralString types */ +export function Capitalize(T: T, options: SchemaOptions = {}): TCapitalize { + return Intrinsic(T, 'Capitalize', options) +} diff --git a/src/type/intrinsic/index.ts b/src/type/intrinsic/index.ts new file mode 100644 index 000000000..c55fa12d6 --- /dev/null +++ b/src/type/intrinsic/index.ts @@ -0,0 +1,34 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './capitalize' +export * from './intrinsic-from-mapped-key' +export * from './intrinsic' +export * from './lowercase' +export * from './uncapitalize' +export * from './uppercase' diff --git a/src/type/intrinsic/intrinsic-from-mapped-key.ts b/src/type/intrinsic/intrinsic-from-mapped-key.ts new file mode 100644 index 000000000..80c873451 --- /dev/null +++ b/src/type/intrinsic/intrinsic-from-mapped-key.ts @@ -0,0 +1,116 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { SchemaOptions } from '../schema/index' +import type { TProperties } from '../object/index' +import { Assert } from '../helpers/index' +import { MappedResult, type TMappedResult, type TMappedKey } from '../mapped/index' +import { Intrinsic, type TIntrinsic, type IntrinsicMode } from './intrinsic' +import { Literal, type TLiteral, type TLiteralValue } from '../literal/index' +import { Clone } from '../clone/value' + +// ------------------------------------------------------------------ +// MappedIntrinsicPropertyKey +// ------------------------------------------------------------------ +// prettier-ignore +type TMappedIntrinsicPropertyKey< + K extends PropertyKey, + M extends IntrinsicMode, +> = { + [_ in K]: TIntrinsic>, M> + } +// prettier-ignore +function MappedIntrinsicPropertyKey< + K extends PropertyKey, + M extends IntrinsicMode, +>(K: K, M: M, options: SchemaOptions): TMappedIntrinsicPropertyKey { + return { + [K]: Intrinsic(Literal(K as TLiteralValue), M, Clone(options)) + } as never +} +// ------------------------------------------------------------------ +// MappedIntrinsicPropertyKeys +// ------------------------------------------------------------------ +// prettier-ignore +type TMappedIntrinsicPropertyKeys< + K extends PropertyKey[], + M extends IntrinsicMode, + Acc extends TProperties = {} +> = ( + K extends [infer L extends PropertyKey, ...infer R extends PropertyKey[]] + ? TMappedIntrinsicPropertyKeys> + : Acc +) +// prettier-ignore +function MappedIntrinsicPropertyKeys< + K extends PropertyKey[], + M extends IntrinsicMode +>(K: [...K], M: M, options: SchemaOptions): TMappedIntrinsicPropertyKeys { + const result = K.reduce((Acc, L) => { + return { ...Acc, ...MappedIntrinsicPropertyKey(L, M, options) } + }, {} as TProperties) as never + + return result +} +// ------------------------------------------------------------------ +// MappedIntrinsicProperties +// ------------------------------------------------------------------ +// prettier-ignore +type TMappedIntrinsicProperties< + K extends TMappedKey, + M extends IntrinsicMode, +> = ( + TMappedIntrinsicPropertyKeys + ) +// prettier-ignore +function MappedIntrinsicProperties< + K extends TMappedKey, + M extends IntrinsicMode, +>(T: K, M: M, options: SchemaOptions): TMappedIntrinsicProperties { + return MappedIntrinsicPropertyKeys(T['keys'], M, options) as never +} +// ------------------------------------------------------------------ +// IntrinsicFromMappedKey +// ------------------------------------------------------------------ +// prettier-ignore +export type TIntrinsicFromMappedKey< + K extends TMappedKey, + M extends IntrinsicMode, + P extends TProperties = TMappedIntrinsicProperties +> = ( + TMappedResult

+) +// prettier-ignore +export function IntrinsicFromMappedKey< + K extends TMappedKey, + M extends IntrinsicMode, + P extends TProperties = TMappedIntrinsicProperties +>(T: K, M: M, options: SchemaOptions): TMappedResult

{ + const P = MappedIntrinsicProperties(T, M, options) + return MappedResult(P) as never +} diff --git a/src/type/intrinsic/intrinsic.ts b/src/type/intrinsic/intrinsic.ts new file mode 100644 index 000000000..aca5d386c --- /dev/null +++ b/src/type/intrinsic/intrinsic.ts @@ -0,0 +1,151 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { CreateType } from '../create/type' +import type { TSchema, SchemaOptions } from '../schema/index' +import { TemplateLiteral, TemplateLiteralParseExact, IsTemplateLiteralExpressionFinite, TemplateLiteralExpressionGenerate, type TTemplateLiteral, type TTemplateLiteralKind } from '../template-literal/index' +import { IntrinsicFromMappedKey, type TIntrinsicFromMappedKey } from './intrinsic-from-mapped-key' +import { Literal, type TLiteral, type TLiteralValue } from '../literal/index' +import { Union, type TUnion } from '../union/index' +import { type TMappedKey } from '../mapped/index' + +// ------------------------------------------------------------------ +// TypeGuard +// ------------------------------------------------------------------ +import { IsMappedKey, IsTemplateLiteral, IsUnion, IsLiteral } from '../guard/kind' +// ------------------------------------------------------------------ +// Apply +// ------------------------------------------------------------------ +function ApplyUncapitalize(value: string): string { + const [first, rest] = [value.slice(0, 1), value.slice(1)] + return [first.toLowerCase(), rest].join('') +} +function ApplyCapitalize(value: string): string { + const [first, rest] = [value.slice(0, 1), value.slice(1)] + return [first.toUpperCase(), rest].join('') +} +function ApplyUppercase(value: string): string { + return value.toUpperCase() +} +function ApplyLowercase(value: string): string { + return value.toLowerCase() +} +// ------------------------------------------------------------------ +// IntrinsicMode +// ------------------------------------------------------------------ +export type IntrinsicMode = 'Uppercase' | 'Lowercase' | 'Capitalize' | 'Uncapitalize' +// ------------------------------------------------------------------ +// FromTemplateLiteral +// ------------------------------------------------------------------ +// prettier-ignore +type TFromTemplateLiteral = + M extends IntrinsicMode ? + T extends [infer L extends TTemplateLiteralKind, ...infer R extends TTemplateLiteralKind[]] + ? [TIntrinsic, ...TFromTemplateLiteral] + : T + : T +function FromTemplateLiteral(schema: TTemplateLiteral, mode: IntrinsicMode, options: SchemaOptions): TFromTemplateLiteral { + // note: template literals require special runtime handling as they are encoded in string patterns. + // This diverges from the mapped type which would otherwise map on the template literal kind. + const expression = TemplateLiteralParseExact(schema.pattern) + const finite = IsTemplateLiteralExpressionFinite(expression) + if (!finite) return { ...schema, pattern: FromLiteralValue(schema.pattern, mode) } as any + const strings = [...TemplateLiteralExpressionGenerate(expression)] + const literals = strings.map((value) => Literal(value)) + const mapped = FromRest(literals as any, mode) + const union = Union(mapped) + return TemplateLiteral([union], options) as never +} +// ------------------------------------------------------------------ +// FromLiteralValue +// ------------------------------------------------------------------ +// prettier-ignore +type TFromLiteralValue = ( + T extends string ? + M extends 'Uncapitalize' ? Uncapitalize : + M extends 'Capitalize' ? Capitalize : + M extends 'Uppercase' ? Uppercase : + M extends 'Lowercase' ? Lowercase : + string + : T +) +// prettier-ignore +function FromLiteralValue(value: TLiteralValue, mode: IntrinsicMode) { + return ( + typeof value === 'string' ? ( + mode === 'Uncapitalize' ? ApplyUncapitalize(value) : + mode === 'Capitalize' ? ApplyCapitalize(value) : + mode === 'Uppercase' ? ApplyUppercase(value) : + mode === 'Lowercase' ? ApplyLowercase(value) : + value + ) : value.toString() + ) +} +// ------------------------------------------------------------------ +// FromRest +// ------------------------------------------------------------------ +// prettier-ignore +type TFromRest = + T extends [infer L extends TSchema, ...infer R extends TSchema[]] + ? TFromRest]> + : Acc +// prettier-ignore +function FromRest(T: [...T], M: M): TFromRest { + return T.map(L => Intrinsic(L, M)) as never +} +// ------------------------------------------------------------------ +// TIntrinsic +// ------------------------------------------------------------------ +// prettier-ignore +export type TIntrinsic = + // Intrinsic-Mapped-Inference + T extends TMappedKey ? TIntrinsicFromMappedKey : + // Standard-Inference + T extends TTemplateLiteral ? TTemplateLiteral> : + T extends TUnion ? TUnion> : + T extends TLiteral ? TLiteral> : + T + +/** Applies an intrinsic string manipulation to the given type. */ +export function Intrinsic(schema: T, mode: M, options?: SchemaOptions): TIntrinsicFromMappedKey +/** Applies an intrinsic string manipulation to the given type. */ +export function Intrinsic(schema: T, mode: M, options?: SchemaOptions): TIntrinsic +/** Applies an intrinsic string manipulation to the given type. */ +export function Intrinsic(schema: TSchema, mode: IntrinsicMode, options: SchemaOptions = {}): never { + // prettier-ignore + return ( + // Intrinsic-Mapped-Inference + IsMappedKey(schema) ? IntrinsicFromMappedKey(schema, mode, options) : + // Standard-Inference + IsTemplateLiteral(schema) ? FromTemplateLiteral(schema, mode, options) : + IsUnion(schema) ? Union(FromRest(schema.anyOf, mode), options) : + IsLiteral(schema) ? Literal(FromLiteralValue(schema.const, mode), options) : + // Default Type + CreateType(schema, options) + ) as never +} diff --git a/src/type/intrinsic/lowercase.ts b/src/type/intrinsic/lowercase.ts new file mode 100644 index 000000000..6837496f8 --- /dev/null +++ b/src/type/intrinsic/lowercase.ts @@ -0,0 +1,37 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { TSchema, SchemaOptions } from '../schema/index' +import { Intrinsic, type TIntrinsic } from './intrinsic' + +// prettier-ignore +export type TLowercase = TIntrinsic +/** `[Json]` Intrinsic function to Lowercase LiteralString types */ +export function Lowercase(T: T, options: SchemaOptions = {}): TLowercase { + return Intrinsic(T, 'Lowercase', options) +} diff --git a/src/type/intrinsic/uncapitalize.ts b/src/type/intrinsic/uncapitalize.ts new file mode 100644 index 000000000..c9af91f12 --- /dev/null +++ b/src/type/intrinsic/uncapitalize.ts @@ -0,0 +1,37 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { TSchema, SchemaOptions } from '../schema/index' +import { Intrinsic, type TIntrinsic } from './intrinsic' + +// prettier-ignore +export type TUncapitalize = TIntrinsic +/** `[Json]` Intrinsic function to Uncapitalize LiteralString types */ +export function Uncapitalize(T: T, options: SchemaOptions = {}): TUncapitalize { + return Intrinsic(T, 'Uncapitalize', options) +} diff --git a/src/type/intrinsic/uppercase.ts b/src/type/intrinsic/uppercase.ts new file mode 100644 index 000000000..34eb578bc --- /dev/null +++ b/src/type/intrinsic/uppercase.ts @@ -0,0 +1,37 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { TSchema, SchemaOptions } from '../schema/index' +import { Intrinsic, type TIntrinsic } from './intrinsic' + +// prettier-ignore +export type TUppercase = TIntrinsic +/** `[Json]` Intrinsic function to Uppercase LiteralString types */ +export function Uppercase(T: T, options: SchemaOptions = {}): TUppercase { + return Intrinsic(T, 'Uppercase', options) +} diff --git a/src/type/iterator/index.ts b/src/type/iterator/index.ts new file mode 100644 index 000000000..03694ff57 --- /dev/null +++ b/src/type/iterator/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './iterator' diff --git a/src/type/iterator/iterator.ts b/src/type/iterator/iterator.ts new file mode 100644 index 000000000..f2d5fd99f --- /dev/null +++ b/src/type/iterator/iterator.ts @@ -0,0 +1,43 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { CreateType } from '../create/type' +import type { TSchema, SchemaOptions } from '../schema/index' +import type { Static } from '../static/index' +import { Kind } from '../symbols/index' + +export interface TIterator extends TSchema { + [Kind]: 'Iterator' + static: IterableIterator> + type: 'Iterator' + items: T +} +/** `[JavaScript]` Creates an Iterator type */ +export function Iterator(items: T, options?: SchemaOptions): TIterator { + return CreateType({ [Kind]: 'Iterator', type: 'Iterator', items }, options) as never +} diff --git a/src/type/keyof/index.ts b/src/type/keyof/index.ts new file mode 100644 index 000000000..8480f6853 --- /dev/null +++ b/src/type/keyof/index.ts @@ -0,0 +1,32 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './keyof-from-mapped-result' +export * from './keyof-property-entries' +export * from './keyof-property-keys' +export * from './keyof' diff --git a/src/type/keyof/keyof-from-mapped-result.ts b/src/type/keyof/keyof-from-mapped-result.ts new file mode 100644 index 000000000..fb96a9ba7 --- /dev/null +++ b/src/type/keyof/keyof-from-mapped-result.ts @@ -0,0 +1,76 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { SchemaOptions } from '../schema/index' +import type { Ensure, Evaluate } from '../helpers/index' +import type { TProperties } from '../object/index' +import { MappedResult, type TMappedResult } from '../mapped/index' +import { KeyOf, type TKeyOfFromType } from './keyof' +import { Clone } from '../clone/value' +// ------------------------------------------------------------------ +// FromProperties +// ------------------------------------------------------------------ +// prettier-ignore +type TFromProperties = ( + { [K2 in keyof Properties]: TKeyOfFromType } +) +// prettier-ignore +function FromProperties(properties: Properties, options?: SchemaOptions): TFromProperties { + const result = {} as TProperties + for(const K2 of globalThis.Object.getOwnPropertyNames(properties)) result[K2] = KeyOf(properties[K2], Clone(options)) + return result as never +} +// ------------------------------------------------------------------ +// FromMappedResult +// ------------------------------------------------------------------ +// prettier-ignore +type TFromMappedResult = ( + Evaluate> +) +// prettier-ignore +function FromMappedResult(mappedResult: MappedResult, options?: SchemaOptions): TFromMappedResult { + return FromProperties(mappedResult.properties, options) as never +} +// ------------------------------------------------------------------ +// KeyOfFromMappedResult +// ------------------------------------------------------------------ +// prettier-ignore +export type TKeyOfFromMappedResult< + MappedResult extends TMappedResult, + Properties extends TProperties = TFromMappedResult +> = ( + Ensure> +) +// prettier-ignore +export function KeyOfFromMappedResult< + MappedResult extends TMappedResult, + Properties extends TProperties = TFromMappedResult +>(mappedResult: MappedResult, options?: SchemaOptions): TMappedResult { + const properties = FromMappedResult(mappedResult, options) + return MappedResult(properties) as never +} diff --git a/src/type/keyof/keyof-property-entries.ts b/src/type/keyof/keyof-property-entries.ts new file mode 100644 index 000000000..8e2361c1b --- /dev/null +++ b/src/type/keyof/keyof-property-entries.ts @@ -0,0 +1,42 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { IndexFromPropertyKeys } from '../indexed/indexed' +import { KeyOfPropertyKeys } from './keyof-property-keys' +import { TSchema } from '../schema/index' + +/** + * `[Utility]` Resolves an array of keys and schemas from the given schema. This method is faster + * than obtaining the keys and resolving each individually via indexing. This method was written + * accellerate Intersect and Union encoding. + */ +export function KeyOfPropertyEntries(schema: TSchema): [key: string, schema: TSchema][] { + const keys = KeyOfPropertyKeys(schema) as string[] + const schemas = IndexFromPropertyKeys(schema, keys) + return keys.map((_, index) => [keys[index], schemas[index]]) +} diff --git a/src/type/keyof/keyof-property-keys.ts b/src/type/keyof/keyof-property-keys.ts new file mode 100644 index 000000000..0130ff934 --- /dev/null +++ b/src/type/keyof/keyof-property-keys.ts @@ -0,0 +1,174 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { TSchema } from '../schema/index' +import { type ZeroString, type UnionToTuple, type TIncrement } from '../helpers/index' +import type { TRecursive } from '../recursive/index' +import type { TIntersect } from '../intersect/index' +import type { TUnion } from '../union/index' +import type { TTuple } from '../tuple/index' +import type { TArray } from '../array/index' +import type { TObject, TProperties } from '../object/index' +import { SetUnionMany, SetIntersectMany, type TSetUnionMany, type TSetIntersectMany } from '../sets/index' + +// ------------------------------------------------------------------ +// TypeGuard +// ------------------------------------------------------------------ +import { IsIntersect, IsUnion, IsTuple, IsArray, IsObject, IsRecord } from '../guard/kind' +// ------------------------------------------------------------------ +// FromRest +// ------------------------------------------------------------------ +// prettier-ignore +type TFromRest = ( + Types extends [infer L extends TSchema, ...infer R extends TSchema[]] + ? TFromRest]> + : Result +) +// prettier-ignore +function FromRest(types: [...Types]): TFromRest { + const result = [] as PropertyKey[][] + for(const L of types) result.push(KeyOfPropertyKeys(L)) + return result as never +} +// ------------------------------------------------------------------ +// FromIntersect +// ------------------------------------------------------------------ +// prettier-ignore +type TFromIntersect, + PropertyKeys extends PropertyKey[] = TSetUnionMany +> = PropertyKeys +// prettier-ignore +function FromIntersect(types: [...Types]): TFromIntersect { + const propertyKeysArray = FromRest(types) as PropertyKey[][] + const propertyKeys = SetUnionMany(propertyKeysArray) + return propertyKeys as never +} +// ------------------------------------------------------------------ +// FromUnion +// ------------------------------------------------------------------ +// prettier-ignore +type TFromUnion, + PropertyKeys extends PropertyKey[] = TSetIntersectMany +> = PropertyKeys +// prettier-ignore +function FromUnion(types: [...Types]): TFromUnion { + const propertyKeysArray = FromRest(types) as PropertyKey[][] + const propertyKeys = SetIntersectMany(propertyKeysArray) + return propertyKeys as never +} +// ------------------------------------------------------------------ +// FromTuple +// ------------------------------------------------------------------ +// prettier-ignore +type TFromTuple = + Types extends [infer _ extends TSchema, ...infer R extends TSchema[]] + ? TFromTuple, [...Acc, Indexer]> + : Acc +// prettier-ignore +function FromTuple(types: [...Types]): TFromTuple { + return types.map((_, indexer) => indexer.toString()) as never +} +// ------------------------------------------------------------------ +// FromArray +// ------------------------------------------------------------------ +// prettier-ignore +type TFromArray<_ extends TSchema> = ( + ['[number]'] +) +// prettier-ignore +function FromArray<_ extends TSchema>(_: _): TFromArray<_> { + return ( + ['[number]'] + ) +} +// ------------------------------------------------------------------ +// FromProperties +// ------------------------------------------------------------------ +// prettier-ignore +type TFromProperties = ( + UnionToTuple +) +// prettier-ignore +function FromProperties(T: Properties): TFromProperties { + return ( + globalThis.Object.getOwnPropertyNames(T) + ) as never +} +// ------------------------------------------------------------------ +// FromPatternProperties +// ------------------------------------------------------------------ +// prettier-ignore +function FromPatternProperties(patternProperties: Record): string[] { + if(!includePatternProperties) return [] + const patternPropertyKeys = globalThis.Object.getOwnPropertyNames(patternProperties) + return patternPropertyKeys.map(key => { + return (key[0] === '^' && key[key.length - 1] === '$') + ? key.slice(1, key.length - 1) + : key + }) +} +// ------------------------------------------------------------------ +// KeyOfPropertyKeys +// ------------------------------------------------------------------ +// prettier-ignore +export type TKeyOfPropertyKeys = ( + Type extends TRecursive ? TKeyOfPropertyKeys : + Type extends TIntersect ? TFromIntersect : + Type extends TUnion ? TFromUnion : + Type extends TTuple ? TFromTuple : + Type extends TArray ? TFromArray : + Type extends TObject ? TFromProperties : + [] +) +/** Returns a tuple of PropertyKeys derived from the given TSchema. */ +// prettier-ignore +export function KeyOfPropertyKeys(type: Type): TKeyOfPropertyKeys { + return ( + IsIntersect(type) ? FromIntersect(type.allOf) : + IsUnion(type) ? FromUnion(type.anyOf) : + IsTuple(type) ? FromTuple(type.items ?? []) : + IsArray(type) ? FromArray(type.items) : + IsObject(type) ? FromProperties(type.properties) : + IsRecord(type) ? FromPatternProperties(type.patternProperties) : + [] + ) as never +} +// ---------------------------------------------------------------- +// KeyOfPattern +// ---------------------------------------------------------------- +let includePatternProperties = false +/** Returns a regular expression pattern derived from the given TSchema */ +export function KeyOfPattern(schema: TSchema): string { + includePatternProperties = true + const keys = KeyOfPropertyKeys(schema) + includePatternProperties = false + const pattern = keys.map((key) => `(${key})`) + return `^(${pattern.join('|')})$` +} diff --git a/src/type/keyof/keyof.ts b/src/type/keyof/keyof.ts new file mode 100644 index 000000000..9a8b26d30 --- /dev/null +++ b/src/type/keyof/keyof.ts @@ -0,0 +1,113 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { CreateType } from '../create/type' +import type { TSchema } from '../schema/index' +import type { Assert, Ensure } from '../helpers/index' +import type { TMappedResult } from '../mapped/index' +import type { SchemaOptions } from '../schema/index' +import { Literal, type TLiteral, type TLiteralValue } from '../literal/index' +import { Number, type TNumber } from '../number/index' +import { Computed, TComputed } from '../computed/index' +import { Ref, type TRef } from '../ref/index' +import { KeyOfPropertyKeys, type TKeyOfPropertyKeys } from './keyof-property-keys' +import { UnionEvaluated, type TUnionEvaluated } from '../union/index' +import { KeyOfFromMappedResult, type TKeyOfFromMappedResult } from './keyof-from-mapped-result' + +// ------------------------------------------------------------------ +// TypeGuard +// ------------------------------------------------------------------ +import { IsMappedResult, IsRef, IsComputed } from '../guard/kind' +// ------------------------------------------------------------------ +// FromComputed +// ------------------------------------------------------------------ +// prettier-ignore +type TFromComputed = Ensure< + TComputed<'KeyOf', [TComputed]> +> +// prettier-ignore +function FromComputed(target: Target, parameters: Parameters): TFromComputed { + return Computed('KeyOf', [Computed(target, parameters)]) as never +} +// ------------------------------------------------------------------ +// FromRef +// ------------------------------------------------------------------ +// prettier-ignore +type TFromRef = Ensure< + TComputed<'KeyOf', [TRef]> +> +// prettier-ignore +function FromRef($ref: Ref): TFromRef { + return Computed('KeyOf', [Ref($ref)]) as never +} +// ------------------------------------------------------------------ +// KeyOfFromType +// ------------------------------------------------------------------ +// prettier-ignore +/** `[Internal]` Used by KeyOfFromMappedResult */ +export type TKeyOfFromType, + PropertyKeyTypes extends TSchema[] = TKeyOfPropertyKeysToRest, + Result = TUnionEvaluated +> = Ensure +// prettier-ignore +function KeyOfFromType(type: Type, options?: SchemaOptions): TKeyOfFromType { + const propertyKeys = KeyOfPropertyKeys(type) as PropertyKey[] + const propertyKeyTypes = KeyOfPropertyKeysToRest(propertyKeys) + const result = UnionEvaluated(propertyKeyTypes) + return CreateType(result, options) as never +} +// ------------------------------------------------------------------ +// FromPropertyKeys +// ------------------------------------------------------------------ +// prettier-ignore +export type TKeyOfPropertyKeysToRest = ( + PropertyKeys extends [infer L extends PropertyKey, ...infer R extends PropertyKey[]] + ? L extends '[number]' + ? TKeyOfPropertyKeysToRest + : TKeyOfPropertyKeysToRest>]> + : Result +) +// prettier-ignore +export function KeyOfPropertyKeysToRest(propertyKeys: [...PropertyKeys]): TKeyOfPropertyKeysToRest { + return propertyKeys.map(L => L === '[number]' ? Number() : Literal(L as TLiteralValue)) as never +} +// ------------------------------------------------------------------ +// TKeyOf +// ------------------------------------------------------------------ +// prettier-ignore +export type TKeyOf = ( + Type extends TComputed ? TFromComputed : + Type extends TRef ? TFromRef : + Type extends TMappedResult ? TKeyOfFromMappedResult : + TKeyOfFromType +) +/** `[Json]` Creates a KeyOf type */ +export function KeyOf(type: Type, options?: SchemaOptions): TKeyOf { + return (IsComputed(type) ? FromComputed(type.target, type.parameters) : IsRef(type) ? FromRef(type.$ref) : IsMappedResult(type) ? KeyOfFromMappedResult(type, options) : KeyOfFromType(type, options)) as never +} diff --git a/src/type/literal/index.ts b/src/type/literal/index.ts new file mode 100644 index 000000000..0892b18c8 --- /dev/null +++ b/src/type/literal/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './literal' diff --git a/src/type/literal/literal.ts b/src/type/literal/literal.ts new file mode 100644 index 000000000..32e1eed20 --- /dev/null +++ b/src/type/literal/literal.ts @@ -0,0 +1,56 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { CreateType } from '../create/type' +import type { TSchema, SchemaOptions } from '../schema/index' +import { Kind } from '../symbols/index' + +// ------------------------------------------------------------------ +// TLiteralValue +// ------------------------------------------------------------------ +export type TLiteralValue = boolean | number | string // | bigint - supported but variant disable due to potential numeric type conflicts + +// ------------------------------------------------------------------ +// TLiteral +// ------------------------------------------------------------------ +export interface TLiteral extends TSchema { + [Kind]: 'Literal' + static: T + const: T +} +/** `[Json]` Creates a Literal type */ +export function Literal(value: T, options?: SchemaOptions): TLiteral { + return CreateType( + { + [Kind]: 'Literal', + const: value, + type: typeof value as 'string' | 'number' | 'boolean', + }, + options, + ) as never +} diff --git a/src/type/mapped/index.ts b/src/type/mapped/index.ts new file mode 100644 index 000000000..e5bf772b6 --- /dev/null +++ b/src/type/mapped/index.ts @@ -0,0 +1,31 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './mapped-key' +export * from './mapped-result' +export * from './mapped' diff --git a/src/type/mapped/mapped-key.ts b/src/type/mapped/mapped-key.ts new file mode 100644 index 000000000..e8dd24f3f --- /dev/null +++ b/src/type/mapped/mapped-key.ts @@ -0,0 +1,44 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { CreateType } from '../create/type' +import type { TSchema } from '../schema/index' +import { Kind } from '../symbols/index' + +export interface TMappedKey extends TSchema { + [Kind]: 'MappedKey' + static: T[number] + keys: T +} +// prettier-ignore +export function MappedKey(T: [...T]): TMappedKey { + return CreateType({ + [Kind]: 'MappedKey', + keys: T + }) as never +} diff --git a/src/type/mapped/mapped-result.ts b/src/type/mapped/mapped-result.ts new file mode 100644 index 000000000..29d5a3c82 --- /dev/null +++ b/src/type/mapped/mapped-result.ts @@ -0,0 +1,45 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { CreateType } from '../create/type' +import type { TSchema } from '../schema/index' +import type { TProperties } from '../object/index' +import { Kind } from '../symbols/index' + +export interface TMappedResult extends TSchema { + [Kind]: 'MappedResult' + properties: T + static: unknown +} +// prettier-ignore +export function MappedResult(properties: T): TMappedResult { + return CreateType({ + [Kind]: 'MappedResult', + properties + }) as never +} diff --git a/src/type/mapped/mapped.ts b/src/type/mapped/mapped.ts new file mode 100644 index 000000000..5dfc725f9 --- /dev/null +++ b/src/type/mapped/mapped.ts @@ -0,0 +1,271 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { TSchema } from '../schema/index' +import type { Ensure, Evaluate, Assert } from '../helpers/index' +import { Kind, OptionalKind, ReadonlyKind } from '../symbols/index' +import { CreateType } from '../create/type' +import { Discard } from '../discard/index' +// evaluation types +import { Array, type TArray } from '../array/index' +import { AsyncIterator, type TAsyncIterator } from '../async-iterator/index' +import { Constructor, type TConstructor } from '../constructor/index' +import { type TEnum, type TEnumRecord } from '../enum/index' +import { Function as FunctionType, type TFunction } from '../function/index' +import { IndexPropertyKeys, type TIndexPropertyKeys } from '../indexed/index' +import { Intersect, type TIntersect } from '../intersect/index' +import { Iterator, type TIterator } from '../iterator/index' +import { Literal, type TLiteral, type TLiteralValue } from '../literal/index' +import { Object, type TObject, type TProperties, type ObjectOptions } from '../object/index' +import { Optional, type TOptional } from '../optional/index' +import { Promise, type TPromise } from '../promise/index' +import { Readonly, type TReadonly } from '../readonly/index' +import { Tuple, type TTuple } from '../tuple/index' +import { Union, type TUnion } from '../union/index' +// operator +import { SetIncludes, type TSetIncludes } from '../sets/index' +// mapping types +import { MappedResult, type TMappedResult } from './mapped-result' +import type { TMappedKey } from './mapped-key' + +// ------------------------------------------------------------------ +// TypeGuard +// ------------------------------------------------------------------ +import { IsArray, IsAsyncIterator, IsConstructor, IsFunction, IsIntersect, IsIterator, IsReadonly, IsMappedResult, IsMappedKey, IsObject, IsOptional, IsPromise, IsSchema, IsTuple, IsUnion } from '../guard/kind' +// ------------------------------------------------------------------ +// FromMappedResult +// +// We only evaluate the context (K) if it is a keyof P. Otherwise we +// remap the back to a MappedResult with the expectation it will be +// evaluated by an outer context. Note that overlapping keys in the +// outer context may result in incorrect evaluation of the outer +// context. Reproduction code below. +// +// type S = { +// [L in 'A' | 'B']: { +// [R in 'B' | 'C']: [L, R] // issue: overlapping 'B' +// } +// } +// +// ------------------------------------------------------------------ +// prettier-ignore +type TFromMappedResult = ( + K extends keyof P + ? FromSchemaType + : TMappedResult

+) +// prettier-ignore +function FromMappedResult(K: K, P: P): TFromMappedResult { + return ( + K in P + ? FromSchemaType(K, P[K as string]) + : MappedResult(P) + ) as never +} +// ------------------------------------------------------------------ +// MappedKeyToKnownMappedResultProperties +// +// This path is used when K is in the PropertyKey set of P. This is +// the standard (fast) path when not nesting mapped types. +// ------------------------------------------------------------------ +// prettier-ignore +type TMappedKeyToKnownMappedResultProperties = { + [_ in K]: TLiteral> +} +// prettier-ignore +function MappedKeyToKnownMappedResultProperties(K: K): TMappedKeyToKnownMappedResultProperties { + return { [K]: Literal(K as TLiteralValue) } as never +} +// ------------------------------------------------------------------ +// MappedKeyToUnknownMappedResultProperties +// +// This path is used when K is outside the set of P. This indicates +// that the K originates from outside the current mappped type. This +// path is very slow as we need to construct a full MappedResult +// to be evaluated by the exterior mapped type. +// ------------------------------------------------------------------ +// prettier-ignore +type TMappedKeyToUnknownMappedResultProperties

= ( + P extends [infer L extends PropertyKey, ...infer R extends PropertyKey[]] + ? TMappedKeyToUnknownMappedResultProperties> }> + : Acc +) +// prettier-ignore +function MappedKeyToUnknownMappedResultProperties

(P: [...P]): TMappedKeyToUnknownMappedResultProperties

{ + const Acc = {} as Record + for(const L of P) Acc[L] = Literal(L as TLiteralValue) + return Acc as never +} +// ------------------------------------------------------------------ +// MappedKeyToMappedResultProperties +// ------------------------------------------------------------------ +// prettier-ignore +type TMappedKeyToMappedResultProperties = ( + TSetIncludes extends true + ? TMappedKeyToKnownMappedResultProperties + : TMappedKeyToUnknownMappedResultProperties

+) +// prettier-ignore +function MappedKeyToMappedResultProperties(K: K, P: [...P]): TMappedKeyToMappedResultProperties { + return ( + SetIncludes(P, K) + ? MappedKeyToKnownMappedResultProperties(K) + : MappedKeyToUnknownMappedResultProperties(P) + ) as never +} +// prettier-ignore +type TFromMappedKey< + K extends PropertyKey, + P extends PropertyKey[], + R extends TProperties = TMappedKeyToMappedResultProperties +> = ( + TFromMappedResult +) +// prettier-ignore +function FromMappedKey(K: K, P: [...P]): TFromMappedKey { + const R = MappedKeyToMappedResultProperties(K, P) + return FromMappedResult(K, R) as never +} +// ------------------------------------------------------------------ +// FromRest +// ------------------------------------------------------------------ +// prettier-ignore +type TFromRest = ( + T extends [infer L extends TSchema, ...infer R extends TSchema[]] + ? TFromRest]> + : Acc +) +// prettier-ignore +function FromRest(K: K, T: [...T]): TFromRest { + return T.map(L => FromSchemaType(K, L)) as never +} +// ------------------------------------------------------------------ +// FromProperties +// ------------------------------------------------------------------ +// prettier-ignore +type FromProperties +}>> = R +// prettier-ignore +function FromProperties(K: K, T: T): FromProperties { + const Acc = {} as Record + for(const K2 of globalThis.Object.getOwnPropertyNames(T)) Acc[K2] = FromSchemaType(K, T[K2]) + return Acc as never +} +// ------------------------------------------------------------------ +// FromMappedPropertyKey +// ------------------------------------------------------------------ +// prettier-ignore +type FromSchemaType = ( + // unevaluated modifier types + T extends TReadonly ? TReadonly> : + T extends TOptional ? TOptional> : + // unevaluated mapped types + T extends TMappedResult ? TFromMappedResult : + T extends TMappedKey ? TFromMappedKey : + // unevaluated types + T extends TConstructor ? TConstructor, FromSchemaType> : + T extends TFunction ? TFunction, FromSchemaType> : + T extends TAsyncIterator ? TAsyncIterator> : + T extends TIterator ? TIterator> : + T extends TIntersect ? TIntersect> : + // note: special case for enums as these are mapped as union types. + T extends TEnum ? TEnum : + T extends TUnion ? TUnion> : + T extends TTuple ? TTuple> : + T extends TObject ? TObject> : + T extends TArray ? TArray> : + T extends TPromise ? TPromise> : + T +) +// prettier-ignore +function FromSchemaType(K: K, T: T): FromSchemaType { + // required to retain user defined options for mapped type + const options = { ...T } + return ( + // unevaluated modifier types + IsOptional(T) ? Optional(FromSchemaType(K, Discard(T, [OptionalKind]) as TSchema)) : + IsReadonly(T) ? Readonly(FromSchemaType(K, Discard(T, [ReadonlyKind]) as TSchema)) : + // unevaluated mapped types + IsMappedResult(T) ? FromMappedResult(K, T.properties) : + IsMappedKey(T) ? FromMappedKey(K, T.keys) : + // unevaluated types + IsConstructor(T) ? Constructor(FromRest(K, T.parameters), FromSchemaType(K, T.returns), options) : + IsFunction(T) ? FunctionType(FromRest(K, T.parameters), FromSchemaType(K, T.returns), options) : + IsAsyncIterator(T) ? AsyncIterator(FromSchemaType(K, T.items), options) : + IsIterator(T) ? Iterator(FromSchemaType(K, T.items), options) : + IsIntersect(T) ? Intersect(FromRest(K, T.allOf), options) : + IsUnion(T) ? Union(FromRest(K, T.anyOf), options) : + IsTuple(T) ? Tuple(FromRest(K, T.items ?? []), options) : + IsObject(T) ? Object(FromProperties(K, T.properties), options) : + IsArray(T) ? Array(FromSchemaType(K, T.items), options) : + IsPromise(T) ? Promise(FromSchemaType(K, T.item), options) : + T + ) as never +} +// ------------------------------------------------------------------ +// MappedFunctionReturnType +// ------------------------------------------------------------------ +// prettier-ignore +export type TMappedFunctionReturnType = ( + K extends [infer L extends PropertyKey, ...infer R extends PropertyKey[]] + ? TMappedFunctionReturnType }> + : Acc +) +// prettier-ignore +export function MappedFunctionReturnType(K: [...K], T: T): TMappedFunctionReturnType { + const Acc = {} as Record + for(const L of K) Acc[L] = FromSchemaType(L, T) + return Acc as never +} +// ------------------------------------------------------------------ +// TMappedFunction +// ------------------------------------------------------------------ +// prettier-ignore +export type TMappedFunction> = (T: I) => TSchema +// ------------------------------------------------------------------ +// TMapped +// ------------------------------------------------------------------ +// prettier-ignore +export type TMapped< + K extends PropertyKey[], + F extends TMappedFunction, + R extends TProperties = Evaluate>>, +> = Ensure> + +/** `[Json]` Creates a Mapped object type */ +export function Mapped, F extends TMappedFunction = TMappedFunction, R extends TMapped = TMapped>(key: K, map: F, options?: ObjectOptions): R +/** `[Json]` Creates a Mapped object type */ +export function Mapped = TMappedFunction, R extends TMapped = TMapped>(key: [...K], map: F, options?: ObjectOptions): R +/** `[Json]` Creates a Mapped object type */ +export function Mapped(key: any, map: Function, options?: ObjectOptions) { + const K = IsSchema(key) ? IndexPropertyKeys(key) : (key as PropertyKey[]) + const RT = map({ [Kind]: 'MappedKey', keys: K } as TMappedKey) + const R = MappedFunctionReturnType(K, RT) + return Object(R, options) +} diff --git a/src/type/module/compute.ts b/src/type/module/compute.ts new file mode 100644 index 000000000..286665597 --- /dev/null +++ b/src/type/module/compute.ts @@ -0,0 +1,471 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { CreateType } from '../create/index' +import { CloneType } from '../clone/index' +import { Discard } from '../discard/index' +import { Ensure, Evaluate } from '../helpers/index' +import { type TSchema } from '../schema/index' +import { Array, type TArray } from '../array/index' +import { Awaited, type TAwaited } from '../awaited/index' +import { AsyncIterator, type TAsyncIterator } from '../async-iterator/index' +import { TComputed } from '../computed/index' +import { Constructor, type TConstructor } from '../constructor/index' +import { Index, type TIndex, type TIndexPropertyKeys } from '../indexed/index' +import { TEnum, type TEnumRecord } from '../enum/index' +import { Function as FunctionType, type TFunction } from '../function/index' +import { Intersect, type TIntersect, type TIntersectEvaluated } from '../intersect/index' +import { Iterator, type TIterator } from '../iterator/index' +import { KeyOf, type TKeyOf } from '../keyof/index' +import { Object, type TObject, type TProperties } from '../object/index' +import { Omit, type TOmit } from '../omit/index' +import { type TOptional } from '../optional/index' +import { Pick, type TPick } from '../pick/index' +import { Never, type TNever } from '../never/index' +import { Partial, TPartial } from '../partial/index' +import { type TReadonly } from '../readonly/index' +import { RecordValue, RecordPattern, type TRecordOrObject, type TRecord } from '../record/index' +import { type TRef } from '../ref/index' +import { Required, type TRequired } from '../required/index' +import { type TTransform } from '../transform/index' +import { Tuple, type TTuple } from '../tuple/index' +import { Union, type TUnion, type TUnionEvaluated } from '../union/index' + +// ------------------------------------------------------------------ +// Symbols +// ------------------------------------------------------------------ +import { TransformKind, OptionalKind, ReadonlyKind } from '../symbols/index' + +// ------------------------------------------------------------------ +// KindGuard +// ------------------------------------------------------------------ +import * as KindGuard from '../guard/kind' + +// ------------------------------------------------------------------ +// +// DereferenceParameters +// +// Dereferences TComputed parameters. It is important to note that +// dereferencing anything other than these parameters may result +// inference and evaluation problems, potentially resulting in a +// stack overflow. All open TRef types must be preserved !!! +// +// ------------------------------------------------------------------ +// prettier-ignore +type TDereferenceParameters = ( + Types extends [infer Left extends TSchema, ...infer Right extends TSchema[]] + ? Left extends TRef + ? TDereferenceParameters]> + : TDereferenceParameters]> + : Result +) +// prettier-ignore +function DereferenceParameters(moduleProperties: ModuleProperties, types: [...Types]): TSchema[] { + return types.map((type) => { + return KindGuard.IsRef(type) + ? Dereference(moduleProperties, type.$ref) + : FromType(moduleProperties, type) + }) as never +} +// prettier-ignore +type TDereference + ? TDereference + : TFromType + : TNever +)> = Result +// prettier-ignore +function Dereference(moduleProperties: ModuleProperties, ref: Ref): TSchema { + return ( + ref in moduleProperties + ? KindGuard.IsRef(moduleProperties[ref]) + ? Dereference(moduleProperties, moduleProperties[ref].$ref) + : FromType(moduleProperties, moduleProperties[ref]) + : Never() + ) as never +} + +// ------------------------------------------------------------------ +// +// TComputed +// +// The following types support deferred evaluation +// +// ------------------------------------------------------------------ +// ------------------------------------------------------------------ +// Awaited +// ------------------------------------------------------------------ +// prettier-ignore +type TFromAwaited = ( + Parameters extends [infer T0 extends TSchema] ? TAwaited : never +) +// prettier-ignore +function FromAwaited(parameters: Parameters): TFromAwaited { + return Awaited(parameters[0]) as never +} +// ------------------------------------------------------------------ +// Index +// ------------------------------------------------------------------ +// prettier-ignore +type TFromIndex = ( + Parameters extends [infer T0 extends TSchema, infer T1 extends TSchema] + // Note: This inferred result check is required to mitgate a "as never" + // assertion on FromComputed resolution. This should be removed when + // reimplementing TIndex to use TSchema as the primary key indexer. + ? TIndex> extends infer Result extends TSchema + ? Result + : never + : never +) +// prettier-ignore +function FromIndex(parameters: Parameters): TFromIndex { + return Index(parameters[0], parameters[1]) as never +} +// ------------------------------------------------------------------ +// KeyOf +// ------------------------------------------------------------------ +// prettier-ignore +type TFromKeyOf = ( + Parameters extends [infer T0 extends TSchema] ? TKeyOf : never +) +// prettier-ignore +function FromKeyOf(parameters: Parameters): TFromAwaited { + return KeyOf(parameters[0]) as never +} +// ------------------------------------------------------------------ +// Partial +// ------------------------------------------------------------------ +// prettier-ignore +type TFromPartial = ( + Parameters extends [infer T0 extends TSchema] ? TPartial : never +) +// prettier-ignore +function FromPartial(parameters: Parameters): TFromPartial { + return Partial(parameters[0]) as never +} +// ------------------------------------------------------------------ +// Omit +// ------------------------------------------------------------------ +// prettier-ignore +type TFromOmit = ( + Parameters extends [infer T0 extends TSchema, infer T1 extends TSchema] ? TOmit : never +) +// prettier-ignore +function FromOmit(parameters: Parameters): TFromPick { + return Omit(parameters[0], parameters[1]) as never +} +// ------------------------------------------------------------------ +// Pick +// ------------------------------------------------------------------ +// prettier-ignore +type TFromPick = ( + Parameters extends [infer T0 extends TSchema, infer T1 extends TSchema] ? TPick : never +) +// prettier-ignore +function FromPick(parameters: Parameters): TFromPick { + return Pick(parameters[0], parameters[1]) as never +} +// ------------------------------------------------------------------ +// Required +// ------------------------------------------------------------------ +// prettier-ignore +type TFromRequired = ( + Parameters extends [infer T0 extends TSchema] ? TRequired : never +) +// prettier-ignore +function FromRequired(parameters: Parameters): TFromPick { + return Required(parameters[0]) as never +} +// ------------------------------------------------------------------ +// Computed +// ------------------------------------------------------------------ +// prettier-ignore +type TFromComputed +> = ( + Target extends 'Awaited' ? TFromAwaited : + Target extends 'Index' ? TFromIndex : + Target extends 'KeyOf' ? TFromKeyOf : + Target extends 'Partial' ? TFromPartial : + Target extends 'Omit' ? TFromOmit : + Target extends 'Pick' ? TFromPick : + Target extends 'Required' ? TFromRequired : + TNever +) +// prettier-ignore +function FromComputed(moduleProperties: ModuleProperties, target: Target, parameters: Parameters): TFromComputed { + const dereferenced = DereferenceParameters(moduleProperties, parameters) + return ( + target === 'Awaited' ? FromAwaited(dereferenced) : + target === 'Index' ? FromIndex(dereferenced) : + target === 'KeyOf' ? FromKeyOf(dereferenced) : + target === 'Partial' ? FromPartial(dereferenced) : + target === 'Omit' ? FromOmit(dereferenced) : + target === 'Pick' ? FromPick(dereferenced) : + target === 'Required' ? FromRequired(dereferenced) : + Never() + ) as never +} + +// ------------------------------------------------------------------ +// +// Standard Types +// +// The following are the standard mappable types and structures +// +// ------------------------------------------------------------------ + +// ------------------------------------------------------------------ +// Array +// ------------------------------------------------------------------ +// prettier-ignore +type TFromArray = ( + Ensure>> +) +function FromArray(moduleProperties: ModuleProperties, type: Type): TFromArray { + return Array(FromType(moduleProperties, type)) +} +// ------------------------------------------------------------------ +// AsyncIterator +// ------------------------------------------------------------------ +// prettier-ignore +type TFromAsyncIterator = ( + TAsyncIterator> +) +function FromAsyncIterator(moduleProperties: ModuleProperties, type: Type): TFromAsyncIterator { + return AsyncIterator(FromType(moduleProperties, type)) +} +// ------------------------------------------------------------------ +// Constructor +// ------------------------------------------------------------------ +// prettier-ignore +type TFromConstructor = ( + TConstructor, TFromType> +) +// prettier-ignore +function FromConstructor( + moduleProperties: ModuleProperties, + parameters: [...Parameters], + instanceType: InstanceType, +): TFromConstructor { + return Constructor(FromTypes(moduleProperties, parameters as never), FromType(moduleProperties, instanceType) as never) as never +} +// ------------------------------------------------------------------ +// Function +// ------------------------------------------------------------------ +// prettier-ignore +type TFromFunction = Ensure< + Ensure, TFromType>> +> +// prettier-ignore +function FromFunction( + moduleProperties: ModuleProperties, + parameters: [...Parameters], + returnType: ReturnType, +): TFromFunction { + return FunctionType(FromTypes(moduleProperties, parameters as never), FromType(moduleProperties, returnType) as never) as never +} +// ------------------------------------------------------------------ +// Intersect +// ------------------------------------------------------------------ +// prettier-ignore +type TFromIntersect = ( + Ensure>> +) +function FromIntersect(moduleProperties: ModuleProperties, types: [...Types]): TFromIntersect { + return Intersect(FromTypes(moduleProperties, types as never)) as never +} +// ------------------------------------------------------------------ +// Iterator +// ------------------------------------------------------------------ +// prettier-ignore +type TFromIterator = ( + TIterator> +) +function FromIterator(moduleProperties: ModuleProperties, type: Type): TFromIterator { + return Iterator(FromType(moduleProperties, type)) +} +// ------------------------------------------------------------------ +// Object +// ------------------------------------------------------------------ +// prettier-ignore +type TFromObject = Ensure +}>>> +function FromObject(moduleProperties: ModuleProperties, properties: Properties): TFromObject { + return Object( + globalThis.Object.keys(properties).reduce((result, key) => { + return { ...result, [key]: FromType(moduleProperties, properties[key]) as never } + }, {} as TProperties), + ) as never +} +// ------------------------------------------------------------------ +// Record +// +// Note: Varying Runtime and Static path here as we need to retain +// constraints on the Record. This requires remapping the entire +// Record in the Runtime path but where the Static path is merely +// a facade for the patternProperties regular expression. +// ------------------------------------------------------------------ +// prettier-ignore +type TFromRecord> +> = Result +// prettier-ignore +function FromRecord(moduleProperties: ModuleProperties, type: Type): never { + const [value, pattern] = [FromType(moduleProperties, RecordValue(type)), RecordPattern(type)] + const result = CloneType(type) + result.patternProperties[pattern] = value + return result as never +} +// ------------------------------------------------------------------ +// Transform +// ------------------------------------------------------------------ +// prettier-ignore +type TFromTransform + ? TTransform, Output> + : TTransform +> = Result +// prettier-ignore +function FromTransform(moduleProperties: ModuleProperties, transform: Transform): TSchema { + return (KindGuard.IsRef(transform)) + ? { ...Dereference(moduleProperties, transform.$ref), [TransformKind]: transform[TransformKind] } + : transform +} +// ------------------------------------------------------------------ +// Tuple +// ------------------------------------------------------------------ +// prettier-ignore +type TFromTuple = ( + Ensure>> +) +function FromTuple(moduleProperties: ModuleProperties, types: [...Types]): TFromTuple { + return Tuple(FromTypes(moduleProperties, types as never)) as never +} +// ------------------------------------------------------------------ +// Union +// ------------------------------------------------------------------ +// prettier-ignore +type TFromUnion = ( + Ensure>> +) +function FromUnion(moduleProperties: ModuleProperties, types: [...Types]): TFromUnion { + return Union(FromTypes(moduleProperties, types as never)) as never +} +// ------------------------------------------------------------------ +// Types +// ------------------------------------------------------------------ +// prettier-ignore +type TFromTypes = ( + Types extends [infer Left extends TSchema, ...infer Right extends TSchema[]] + ? TFromTypes]> + : Result +) +function FromTypes(moduleProperties: ModuleProperties, types: [...Types]): TFromTypes { + return types.map((type) => FromType(moduleProperties, type)) as never +} +// ------------------------------------------------------------------ +// Type +// ------------------------------------------------------------------ +// prettier-ignore +export type TFromType = ( + // Modifier + Type extends TOptional ? TOptional> : + Type extends TReadonly ? TReadonly> : + // Transform + Type extends TTransform ? TFromTransform : + // Types + Type extends TArray ? TFromArray : + Type extends TAsyncIterator ? TFromAsyncIterator : + Type extends TComputed ? TFromComputed : + Type extends TConstructor ? TFromConstructor : + Type extends TFunction ? TFromFunction : + Type extends TIntersect ? TFromIntersect : + Type extends TIterator ? TFromIterator : + Type extends TObject ? TFromObject : + Type extends TRecord ? TFromRecord : + Type extends TTuple ? TFromTuple : + Type extends TEnum ? Type : // intercept enum before union + Type extends TUnion ? TFromUnion : + Type +) +// prettier-ignore +export function FromType(moduleProperties: ModuleProperties, type: Type): TFromType { + return ( + // Modifiers + KindGuard.IsOptional(type) ? CreateType(FromType(moduleProperties, Discard(type, [OptionalKind]) as TSchema) as never, type) : + KindGuard.IsReadonly(type) ? CreateType(FromType(moduleProperties, Discard(type, [ReadonlyKind]) as TSchema) as never, type) : + // Transform + KindGuard.IsTransform(type) ? CreateType(FromTransform(moduleProperties, type), type) : + // Types + KindGuard.IsArray(type) ? CreateType(FromArray(moduleProperties, type.items), type) : + KindGuard.IsAsyncIterator(type) ? CreateType(FromAsyncIterator(moduleProperties, type.items), type) : + KindGuard.IsComputed(type) ? CreateType(FromComputed(moduleProperties, type.target, type.parameters)) : + KindGuard.IsConstructor(type) ? CreateType(FromConstructor(moduleProperties, type.parameters, type.returns), type) : + KindGuard.IsFunction(type) ? CreateType(FromFunction(moduleProperties, type.parameters, type.returns), type) : + KindGuard.IsIntersect(type) ? CreateType(FromIntersect(moduleProperties, type.allOf), type) : + KindGuard.IsIterator(type) ? CreateType(FromIterator(moduleProperties, type.items), type) : + KindGuard.IsObject(type) ? CreateType(FromObject(moduleProperties, type.properties), type) : + KindGuard.IsRecord(type) ? CreateType(FromRecord(moduleProperties, type)) : + KindGuard.IsTuple(type)? CreateType(FromTuple(moduleProperties, type.items || []), type) : + KindGuard.IsUnion(type) ? CreateType(FromUnion(moduleProperties, type.anyOf), type) : + type + ) as never +} +// ------------------------------------------------------------------ +// ComputeType +// ------------------------------------------------------------------ +// prettier-ignore +export type TComputeType = ( + Key extends keyof ModuleProperties + ? TFromType + : TNever +) +// prettier-ignore +export function ComputeType(moduleProperties: ModuleProperties, key: Key): TComputeType { + return ( + key in moduleProperties + ? FromType(moduleProperties, moduleProperties[key as keyof ModuleProperties]) + : Never() + ) as never +} +// ------------------------------------------------------------------ +// ComputeModuleProperties +// ------------------------------------------------------------------ +// prettier-ignore +export type TComputeModuleProperties = Evaluate<{ + [Key in keyof ModuleProperties]: TComputeType +}> +// prettier-ignore +export function ComputeModuleProperties(moduleProperties: ModuleProperties): TComputeModuleProperties { + return globalThis.Object.getOwnPropertyNames(moduleProperties).reduce((result, key) => { + return {...result, [key]: ComputeType(moduleProperties, key) } + }, {} as TProperties) as never +} diff --git a/src/type/module/index.ts b/src/type/module/index.ts new file mode 100644 index 000000000..ba33fc039 --- /dev/null +++ b/src/type/module/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './module' diff --git a/src/type/module/infer.ts b/src/type/module/infer.ts new file mode 100644 index 000000000..8c8708d20 --- /dev/null +++ b/src/type/module/infer.ts @@ -0,0 +1,176 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { Ensure, Evaluate } from '../helpers/index' +import { TSchema } from '../schema/index' +import { TArray } from '../array/index' +import { TAsyncIterator } from '../async-iterator/index' +import { TConstructor } from '../constructor/index' +import { TEnum, TEnumRecord } from '../enum/index' +import { TFunction } from '../function/index' +import { TIntersect } from '../intersect/index' +import { TIterator } from '../iterator/index' +import { TObject, TProperties } from '../object/index' +import { TOptional } from '../optional/index' +import { TRecord } from '../record/index' +import { TReadonly } from '../readonly/index' +import { TRef } from '../ref/index' +import { TTuple } from '../tuple/index' +import { TUnion } from '../union/index' +import { Static } from '../static/index' +import { TRecursive } from '../recursive/index' + +// ------------------------------------------------------------------ +// Array +// ------------------------------------------------------------------ +// prettier-ignore +type TInferArray = ( + Ensure>> +) +// ------------------------------------------------------------------ +// AsyncIterator +// ------------------------------------------------------------------ +// prettier-ignore +type TInferAsyncIterator = ( + Ensure>> +) +// ------------------------------------------------------------------ +// Constructor +// ------------------------------------------------------------------ +// prettier-ignore +type TInferConstructor = Ensure< + new (...args: TInferTuple) => TInfer +> +// ------------------------------------------------------------------ +// Function +// ------------------------------------------------------------------ +// prettier-ignore +type TInferFunction = Ensure< + (...args: TInferTuple) => TInfer +> +// ------------------------------------------------------------------ +// Iterator +// ------------------------------------------------------------------ +// prettier-ignore +type TInferIterator = ( + Ensure>> +) +// ------------------------------------------------------------------ +// Intersect +// ------------------------------------------------------------------ +// prettier-ignore +type TInferIntersect = ( + Types extends [infer Left extends TSchema, ...infer Right extends TSchema[]] + ? TInferIntersect> + : Result +) +// ------------------------------------------------------------------ +// Object +// ------------------------------------------------------------------ +type ReadonlyOptionalPropertyKeys = { [Key in keyof Properties]: Properties[Key] extends TReadonly ? (Properties[Key] extends TOptional ? Key : never) : never }[keyof Properties] +type ReadonlyPropertyKeys = { [Key in keyof Source]: Source[Key] extends TReadonly ? (Source[Key] extends TOptional ? never : Key) : never }[keyof Source] +type OptionalPropertyKeys = { [Key in keyof Source]: Source[Key] extends TOptional ? (Source[Key] extends TReadonly ? never : Key) : never }[keyof Source] +type RequiredPropertyKeys = keyof Omit | ReadonlyPropertyKeys | OptionalPropertyKeys> +// prettier-ignore +type InferPropertiesWithModifiers> = Evaluate<( + Readonly>>> & + Readonly>> & + Partial>> & + Required>> +)> +// prettier-ignore +type InferProperties = InferPropertiesWithModifiers +}> +// prettier-ignore +type TInferObject = ( + InferProperties +) +// ------------------------------------------------------------------ +// Tuple +// ------------------------------------------------------------------ +// prettier-ignore +type TInferTuple = ( + Types extends [infer L extends TSchema, ...infer R extends TSchema[]] + ? TInferTuple]> + : Result +) +// ------------------------------------------------------------------ +// Record +// ------------------------------------------------------------------ +// prettier-ignore +type TInferRecord extends infer Key extends PropertyKey ? Key : never, + InferedType extends unknown = TInfer, +> = Ensure<{ [_ in InferredKey]: InferedType }> +// ------------------------------------------------------------------ +// Ref +// ------------------------------------------------------------------ +// prettier-ignore +type TInferRef = ( + Ref extends keyof ModuleProperties ? TInfer : unknown +) +// ------------------------------------------------------------------ +// Union +// ------------------------------------------------------------------ +// prettier-ignore +type TInferUnion = ( + Types extends [infer L extends TSchema, ...infer R extends TSchema[]] + ? TInferUnion> + : Result +) +// ------------------------------------------------------------------ +// Infer +// ------------------------------------------------------------------ +// prettier-ignore +type TInfer = ( + Type extends TArray ? TInferArray : + Type extends TAsyncIterator ? TInferAsyncIterator : + Type extends TConstructor ? TInferConstructor : + Type extends TFunction ? TInferFunction : + Type extends TIntersect ? TInferIntersect : + Type extends TIterator ? TInferIterator : + Type extends TObject ? TInferObject : + Type extends TRecord ? TInferRecord : + Type extends TRef ? TInferRef : + Type extends TTuple ? TInferTuple : + Type extends TEnum ? Static : // intercept enum before union + Type extends TUnion ? TInferUnion : + Type extends TRecursive ? TInfer : + Static +) +// ------------------------------------------------------------------ +// InferFromModuleKey +// ------------------------------------------------------------------ +/** Inference Path for Imports. This type is used to compute TImport `static` */ +// prettier-ignore +export type TInferFromModuleKey = ( + Key extends keyof ModuleProperties + ? TInfer + : never +) diff --git a/src/type/module/module.ts b/src/type/module/module.ts new file mode 100644 index 000000000..afc34fbc1 --- /dev/null +++ b/src/type/module/module.ts @@ -0,0 +1,84 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { CreateType } from '../create/index' +import { Kind } from '../symbols/index' +import { SchemaOptions, TSchema } from '../schema/index' +import { TProperties } from '../object/index' +import { Static } from '../static/index' + +// ------------------------------------------------------------------ +// Module Infrastructure Types +// ------------------------------------------------------------------ +import { ComputeModuleProperties, TComputeModuleProperties } from './compute' +import { TInferFromModuleKey } from './infer' + +// ------------------------------------------------------------------ +// Definitions +// ------------------------------------------------------------------ +export interface TDefinitions extends TSchema { + static: { [K in keyof ModuleProperties]: Static } + $defs: ModuleProperties +} +// ------------------------------------------------------------------ +// Import +// ------------------------------------------------------------------ +// prettier-ignore +export interface TImport extends TSchema { + [Kind]: 'Import' + static: TInferFromModuleKey + $defs: ModuleProperties + $ref: Key +} +// ------------------------------------------------------------------ +// Module +// ------------------------------------------------------------------ +// prettier-ignore +export class TModule> { + private readonly $defs: ComputedModuleProperties + constructor($defs: ModuleProperties) { + const computed = ComputeModuleProperties($defs) + const identified = this.WithIdentifiers(computed as never) + this.$defs = identified as never + } + /** `[Json]` Imports a Type by Key. */ + public Import(key: Key, options?: SchemaOptions): TImport { + const $defs = { ...this.$defs, [key]: CreateType(this.$defs[key], options) } + return CreateType({ [Kind]: 'Import', $defs, $ref: key }) as never + } + // prettier-ignore + private WithIdentifiers($defs: ComputedModuleProperties): ComputedModuleProperties { + return globalThis.Object.getOwnPropertyNames($defs).reduce((result, key) => { + return { ...result, [key]: { ...$defs[key], $id: key }} + }, {}) as never + } +} +/** `[Json]` Creates a Type Definition Module. */ +export function Module(properties: Properties): TModule { + return new TModule(properties) +} diff --git a/src/type/never/index.ts b/src/type/never/index.ts new file mode 100644 index 000000000..72210457c --- /dev/null +++ b/src/type/never/index.ts @@ -0,0 +1,28 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ +export * from './never' diff --git a/src/type/never/never.ts b/src/type/never/never.ts new file mode 100644 index 000000000..fb75cda84 --- /dev/null +++ b/src/type/never/never.ts @@ -0,0 +1,41 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { CreateType } from '../create/type' +import type { TSchema, SchemaOptions } from '../schema/index' +import { Kind } from '../symbols/index' + +export interface TNever extends TSchema { + [Kind]: 'Never' + static: never + not: {} +} +/** `[Json]` Creates a Never type */ +export function Never(options?: SchemaOptions): TNever { + return CreateType({ [Kind]: 'Never', not: {} }, options) as never +} diff --git a/src/type/not/index.ts b/src/type/not/index.ts new file mode 100644 index 000000000..49e085461 --- /dev/null +++ b/src/type/not/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './not' diff --git a/src/type/not/not.ts b/src/type/not/not.ts new file mode 100644 index 000000000..4ee1ef139 --- /dev/null +++ b/src/type/not/not.ts @@ -0,0 +1,42 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { CreateType } from '../create/type' +import type { TSchema, SchemaOptions } from '../schema/index' +import type { Static } from '../static/index' +import { Kind } from '../symbols/index' + +export interface TNot extends TSchema { + [Kind]: 'Not' + static: T extends TNot ? Static : unknown + not: T +} +/** `[Json]` Creates a Not type */ +export function Not(type: Type, options?: SchemaOptions): TNot { + return CreateType({ [Kind]: 'Not', not: type }, options) as never +} diff --git a/src/type/null/index.ts b/src/type/null/index.ts new file mode 100644 index 000000000..a57dac3de --- /dev/null +++ b/src/type/null/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './null' diff --git a/src/type/null/null.ts b/src/type/null/null.ts new file mode 100644 index 000000000..f51fccfd7 --- /dev/null +++ b/src/type/null/null.ts @@ -0,0 +1,41 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { CreateType } from '../create/type' +import type { TSchema, SchemaOptions } from '../schema/index' +import { Kind } from '../symbols/index' + +export interface TNull extends TSchema { + [Kind]: 'Null' + static: null + type: 'null' +} +/** `[Json]` Creates a Null type */ +export function Null(options?: SchemaOptions): TNull { + return CreateType({ [Kind]: 'Null', type: 'null' }, options) as never +} diff --git a/src/type/number/index.ts b/src/type/number/index.ts new file mode 100644 index 000000000..3d335f9ba --- /dev/null +++ b/src/type/number/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './number' diff --git a/src/type/number/number.ts b/src/type/number/number.ts new file mode 100644 index 000000000..8d92776cc --- /dev/null +++ b/src/type/number/number.ts @@ -0,0 +1,48 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { CreateType } from '../create/type' +import type { TSchema, SchemaOptions } from '../schema/index' +import { Kind } from '../symbols/index' + +export interface NumberOptions extends SchemaOptions { + exclusiveMaximum?: number + exclusiveMinimum?: number + maximum?: number + minimum?: number + multipleOf?: number +} +export interface TNumber extends TSchema, NumberOptions { + [Kind]: 'Number' + static: number + type: 'number' +} +/** `[Json]` Creates a Number type */ +export function Number(options?: NumberOptions): TNumber { + return CreateType({ [Kind]: 'Number', type: 'number' }, options) as never +} diff --git a/src/type/object/index.ts b/src/type/object/index.ts new file mode 100644 index 000000000..97eac7b24 --- /dev/null +++ b/src/type/object/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './object' diff --git a/src/type/object/object.ts b/src/type/object/object.ts new file mode 100644 index 000000000..a0745ee25 --- /dev/null +++ b/src/type/object/object.ts @@ -0,0 +1,101 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { CreateType } from '../create/type' +import type { TSchema, SchemaOptions } from '../schema/index' +import type { Static } from '../static/index' +import type { Evaluate } from '../helpers/index' +import type { TReadonly } from '../readonly/index' +import type { TOptional } from '../optional/index' +import { Kind } from '../symbols/index' + +// ------------------------------------------------------------------ +// TypeGuard +// ------------------------------------------------------------------ +import { IsOptional } from '../guard/kind' + +// ------------------------------------------------------------------ +// ObjectStatic +// ------------------------------------------------------------------ +type ReadonlyOptionalPropertyKeys = { [K in keyof T]: T[K] extends TReadonly ? (T[K] extends TOptional ? K : never) : never }[keyof T] +type ReadonlyPropertyKeys = { [K in keyof T]: T[K] extends TReadonly ? (T[K] extends TOptional ? never : K) : never }[keyof T] +type OptionalPropertyKeys = { [K in keyof T]: T[K] extends TOptional ? (T[K] extends TReadonly ? never : K) : never }[keyof T] +type RequiredPropertyKeys = keyof Omit | ReadonlyPropertyKeys | OptionalPropertyKeys> + +// prettier-ignore +type ObjectStaticProperties> = Evaluate<( + Readonly>>> & + Readonly>> & + Partial>> & + Required>> +)> +// prettier-ignore +type ObjectStatic = ObjectStaticProperties +}> +// ------------------------------------------------------------------ +// TProperties +// ------------------------------------------------------------------ +export type TPropertyKey = string | number // Consider making this PropertyKey +export type TProperties = Record +// ------------------------------------------------------------------ +// TObject +// ------------------------------------------------------------------ +export type TAdditionalProperties = undefined | TSchema | boolean +export interface ObjectOptions extends SchemaOptions { + /** Additional property constraints for this object */ + additionalProperties?: TAdditionalProperties + /** The minimum number of properties allowed on this object */ + minProperties?: number + /** The maximum number of properties allowed on this object */ + maxProperties?: number +} +export interface TObject extends TSchema, ObjectOptions { + [Kind]: 'Object' + static: ObjectStatic + additionalProperties?: TAdditionalProperties + type: 'object' + properties: T + required?: string[] +} +function RequiredKeys(properties: T): string[] { + const keys: string[] = [] + for (let key in properties) { + if (!IsOptional(properties[key])) keys.push(key) + } + return keys +} +/** `[Json]` Creates an Object type */ +function _Object(properties: T, options?: ObjectOptions): TObject { + const required = RequiredKeys(properties) + const schematic = required.length > 0 ? { [Kind]: 'Object', type: 'object', properties, required } : { [Kind]: 'Object', type: 'object', properties } + return CreateType(schematic, options) as never +} + +/** `[Json]` Creates an Object type */ +export var Object = _Object diff --git a/src/type/omit/index.ts b/src/type/omit/index.ts new file mode 100644 index 000000000..40be9e17a --- /dev/null +++ b/src/type/omit/index.ts @@ -0,0 +1,31 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './omit-from-mapped-key' +export * from './omit-from-mapped-result' +export * from './omit' diff --git a/src/type/omit/omit-from-mapped-key.ts b/src/type/omit/omit-from-mapped-key.ts new file mode 100644 index 000000000..08df5f145 --- /dev/null +++ b/src/type/omit/omit-from-mapped-key.ts @@ -0,0 +1,87 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { TSchema, SchemaOptions } from '../schema/index' +import type { TProperties } from '../object/index' +import { MappedResult, type TMappedResult, type TMappedKey } from '../mapped/index' +import { Omit, type TOmit } from './omit' +import { Clone } from '../clone/value' + +// ------------------------------------------------------------------ +// FromPropertyKey +// ------------------------------------------------------------------ +// prettier-ignore +type TFromPropertyKey = { + [_ in Key]: TOmit +} +// prettier-ignore +function FromPropertyKey(type: Type, key: Key, options?: SchemaOptions): TFromPropertyKey { + return { [key]: Omit(type, [key], Clone(options)) } as never +} +// ------------------------------------------------------------------ +// FromPropertyKeys +// ------------------------------------------------------------------ +// prettier-ignore +type TFromPropertyKeys = ( + PropertyKeys extends [infer LK extends PropertyKey, ...infer RK extends PropertyKey[]] + ? TFromPropertyKeys> + : Result +) +// prettier-ignore +function FromPropertyKeys(type: Type, propertyKeys: [...PropertyKeys], options?: SchemaOptions): TFromPropertyKeys { + return propertyKeys.reduce((Acc, LK) => { + return { ...Acc, ...FromPropertyKey(type, LK, options) } + }, {} as TProperties) as never +} +// ------------------------------------------------------------------ +// FromMappedKey +// ------------------------------------------------------------------ +// prettier-ignore +type TFromMappedKey = ( + TFromPropertyKeys +) +// prettier-ignore +function FromMappedKey(type: Type, mappedKey: MappedKey, options?: SchemaOptions): TFromMappedKey { + return FromPropertyKeys(type, mappedKey.keys, options) as never +} +// ------------------------------------------------------------------ +// OmitFromMappedKey +// ------------------------------------------------------------------ +// prettier-ignore +export type TOmitFromMappedKey +> = ( + TMappedResult +) +// prettier-ignore +export function OmitFromMappedKey +>(type: Type, mappedKey: MappedKey, options?: SchemaOptions): TMappedResult { + const properties = FromMappedKey(type, mappedKey, options) + return MappedResult(properties) as never +} diff --git a/src/type/omit/omit-from-mapped-result.ts b/src/type/omit/omit-from-mapped-result.ts new file mode 100644 index 000000000..4bc99f029 --- /dev/null +++ b/src/type/omit/omit-from-mapped-result.ts @@ -0,0 +1,79 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { SchemaOptions } from '../schema/index' +import type { Ensure, Evaluate } from '../helpers/index' +import type { TProperties } from '../object/index' +import { MappedResult, type TMappedResult } from '../mapped/index' +import { Omit, type TOmit } from './omit' +import { Clone } from '../clone/value' + +// ------------------------------------------------------------------ +// FromProperties +// ------------------------------------------------------------------ +// prettier-ignore +type TFromProperties = ( + { [K2 in keyof Properties]: TOmit } +) +// prettier-ignore +function FromProperties(properties: Properties, propertyKeys: [...PropertyKeys], options?: SchemaOptions): TFromProperties { + const result = {} as TProperties + for(const K2 of globalThis.Object.getOwnPropertyNames(properties)) result[K2] = Omit(properties[K2], propertyKeys, Clone(options)) + return result as never +} +// ------------------------------------------------------------------ +// FromMappedResult +// ------------------------------------------------------------------ +// prettier-ignore +type TFromMappedResult = ( + Evaluate> +) +// prettier-ignore +function FromMappedResult(mappedResult: MappedResult, propertyKeys: [...PropertyKeys], options?: SchemaOptions): TFromMappedResult { + return FromProperties(mappedResult.properties, propertyKeys, options) as never +} +// ------------------------------------------------------------------ +// TOmitFromMappedResult +// ------------------------------------------------------------------ +// prettier-ignore +export type TOmitFromMappedResult< + MappedResult extends TMappedResult, + PropertyKeys extends PropertyKey[], + Properties extends TProperties = TFromMappedResult +> = ( + Ensure> +) +// prettier-ignore +export function OmitFromMappedResult< + MappedResult extends TMappedResult, + PropertyKeys extends PropertyKey[], + Properties extends TProperties = TFromMappedResult +>(mappedResult: MappedResult, propertyKeys: [...PropertyKeys], options?: SchemaOptions): TMappedResult { + const properties = FromMappedResult(mappedResult, propertyKeys, options) + return MappedResult(properties) as never +} diff --git a/src/type/omit/omit.ts b/src/type/omit/omit.ts new file mode 100644 index 000000000..c7c866640 --- /dev/null +++ b/src/type/omit/omit.ts @@ -0,0 +1,194 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { CreateType } from '../create/type' +import { Discard } from '../discard/discard' +import { TransformKind } from '../symbols/symbols' +import type { SchemaOptions, TSchema } from '../schema/index' +import type { TupleToUnion, Evaluate, Ensure } from '../helpers/index' +import { type TRecursive } from '../recursive/index' +import type { TMappedKey, TMappedResult } from '../mapped/index' +import { Computed, TComputed } from '../computed/index' +import { Literal, TLiteral, TLiteralValue } from '../literal/index' +import { IndexPropertyKeys, type TIndexPropertyKeys } from '../indexed/index' +import { Intersect, type TIntersect } from '../intersect/index' +import { Union, type TUnion } from '../union/index' +import { Object, type TObject, type TProperties } from '../object/index' +import { type TRef } from '../ref/index' + +// ------------------------------------------------------------------ +// Mapped +// ------------------------------------------------------------------ +import { OmitFromMappedKey, type TOmitFromMappedKey } from './omit-from-mapped-key' +import { OmitFromMappedResult, type TOmitFromMappedResult } from './omit-from-mapped-result' + +// ------------------------------------------------------------------ +// TypeGuard +// ------------------------------------------------------------------ +import { IsMappedKey, IsIntersect, IsUnion, IsObject, IsSchema, IsMappedResult, IsLiteralValue, IsRef } from '../guard/kind' +import { IsArray as IsArrayValue } from '../guard/value' + +// ------------------------------------------------------------------ +// FromIntersect +// ------------------------------------------------------------------ +// prettier-ignore +type TFromIntersect = ( + Types extends [infer L extends TSchema, ...infer R extends TSchema[]] + ? TFromIntersect]> + : Result +) +// prettier-ignore +function FromIntersect(types: Types, propertyKeys: PropertyKeys) { + return types.map((type) => OmitResolve(type, propertyKeys)) as never +} +// ------------------------------------------------------------------ +// FromUnion +// ------------------------------------------------------------------ +// prettier-ignore +type TFromUnion = ( + T extends [infer L extends TSchema, ...infer R extends TSchema[]] + ? TFromUnion]> + : Result +) +// prettier-ignore +function FromUnion(types: Types, propertyKeys: PropertyKeys) { + return types.map((type) => OmitResolve(type, propertyKeys)) as never +} +// ------------------------------------------------------------------ +// FromProperty +// ------------------------------------------------------------------ +// prettier-ignore +function FromProperty(properties: Properties, key: Key): TProperties { + const { [key]: _, ...R } = properties + return R +} +// prettier-ignore +type TFromProperties> = ( + Evaluate> +) +// prettier-ignore +function FromProperties(properties: Properties, propertyKeys: PropertyKeys) { + return propertyKeys.reduce((T, K2) => FromProperty(T, K2), properties as TProperties) +} +// ------------------------------------------------------------------ +// FromObject +// ------------------------------------------------------------------ +// prettier-ignore +type TFromObject = Ensure +)>> +// prettier-ignore +function FromObject(properties: Properties, propertyKeys: PropertyKeys): TFromObject { + const options = Discard(properties, [TransformKind, '$id', 'required', 'properties']) + const omittedProperties = FromProperties(properties['properties'], propertyKeys) + return Object(omittedProperties, options) as never +} +// ------------------------------------------------------------------ +// UnionFromPropertyKeys +// ------------------------------------------------------------------ +// prettier-ignore +type TUnionFromPropertyKeys = ( + PropertyKeys extends [infer Key extends PropertyKey, ...infer Rest extends PropertyKey[]] + ? Key extends TLiteralValue + ? TUnionFromPropertyKeys]> + : TUnionFromPropertyKeys + : TUnion +) +// prettier-ignore +function UnionFromPropertyKeys(propertyKeys: PropertyKeys): TUnionFromPropertyKeys { + const result = propertyKeys.reduce((result, key) => IsLiteralValue(key) ? [...result, Literal(key)]: result, [] as TLiteral[]) + return Union(result) as never +} +// ------------------------------------------------------------------ +// TOmitResolve +// ------------------------------------------------------------------ +// prettier-ignore +export type TOmitResolve = ( + Properties extends TRecursive ? TRecursive> : + Properties extends TIntersect ? TIntersect> : + Properties extends TUnion ? TUnion> : + Properties extends TObject ? TFromObject, PropertyKeys> : + TObject<{}> +) +// prettier-ignore +function OmitResolve(properties: Properties, propertyKeys: [...PropertyKeys]): TOmitResolve { + return ( + IsIntersect(properties) ? Intersect(FromIntersect(properties.allOf, propertyKeys)) : + IsUnion(properties) ? Union(FromUnion(properties.anyOf, propertyKeys)) : + IsObject(properties) ? FromObject(properties, propertyKeys) : + Object({}) + ) as never +} +// ------------------------------------------------------------------ +// TOmit +// +// This mapping logic is to overly complex because of the decision +// to use PropertyKey[] as the default selector. The PropertyKey[] +// did make TMapped types simpler to implement, but a non-TSchema +// selector makes supporting TComputed awkward as it requires +// generalization via TSchema. This type should be reimplemented +// in the next major revision to support TSchema as the primary +// selector. +// +// ------------------------------------------------------------------ +// prettier-ignore (do not export this type) +type TResolvePropertyKeys = Key extends TSchema ? TIndexPropertyKeys : Key +// prettier-ignore (do not export this type) +type TResolveTypeKey = Key extends PropertyKey[] ? TUnionFromPropertyKeys : Key +// prettier-ignore +export type TOmit = ( + Type extends TMappedResult ? TOmitFromMappedResult> : + Key extends TMappedKey ? TOmitFromMappedKey : + [IsTypeRef, IsKeyRef] extends [true, true] ? TComputed<'Omit', [Type, TResolveTypeKey]> : + [IsTypeRef, IsKeyRef] extends [false, true] ? TComputed<'Omit', [Type, TResolveTypeKey]> : + [IsTypeRef, IsKeyRef] extends [true, false] ? TComputed<'Omit', [Type, TResolveTypeKey]> : + TOmitResolve> +) +/** `[Json]` Constructs a type whose keys are picked from the given type */ +export function Omit(type: Type, key: readonly [...Key], options?: SchemaOptions): TOmit +/** `[Json]` Constructs a type whose keys are picked from the given type */ +export function Omit(type: Type, key: Key, options?: SchemaOptions): TOmit +/** `[Json]` Constructs a type whose keys are picked from the given type */ +// prettier-ignore +export function Omit(type: any, key: any, options?: SchemaOptions): any { + const typeKey: TSchema = IsArrayValue(key) ? UnionFromPropertyKeys(key as PropertyKey[]) : key + const propertyKeys: PropertyKey[] = IsSchema(key) ? IndexPropertyKeys(key) : key + const isTypeRef: boolean = IsRef(type) + const isKeyRef: boolean = IsRef(key) + return ( + IsMappedResult(type) ? OmitFromMappedResult(type, propertyKeys, options) : + IsMappedKey(key) ? OmitFromMappedKey(type, key, options) : + (isTypeRef && isKeyRef) ? Computed('Omit', [type, typeKey], options) : + (!isTypeRef && isKeyRef) ? Computed('Omit', [type, typeKey], options) : + (isTypeRef && !isKeyRef) ? Computed('Omit', [type, typeKey], options) : + CreateType({ ...OmitResolve(type, propertyKeys), ...options }) + ) as never +} diff --git a/src/type/optional/index.ts b/src/type/optional/index.ts new file mode 100644 index 000000000..a9da96fea --- /dev/null +++ b/src/type/optional/index.ts @@ -0,0 +1,30 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './optional-from-mapped-result' +export * from './optional' diff --git a/src/type/optional/optional-from-mapped-result.ts b/src/type/optional/optional-from-mapped-result.ts new file mode 100644 index 000000000..fe04a71fb --- /dev/null +++ b/src/type/optional/optional-from-mapped-result.ts @@ -0,0 +1,88 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { TProperties } from '../object/index' +import { MappedResult, type TMappedResult } from '../mapped/index' +import { Optional, type TOptionalWithFlag } from './optional' + +// ------------------------------------------------------------------ +// FromProperties +// ------------------------------------------------------------------ +// prettier-ignore +type TFromProperties< + P extends TProperties, + F extends boolean, +> = ( + { [K2 in keyof P]: TOptionalWithFlag } +) +// prettier-ignore +function FromProperties< + P extends TProperties, + F extends boolean, +>(P: P, F: F): TFromProperties { + const Acc = {} as TProperties + for(const K2 of globalThis.Object.getOwnPropertyNames(P)) Acc[K2] = Optional(P[K2], F) + return Acc as never +} +// ------------------------------------------------------------------ +// FromMappedResult +// ------------------------------------------------------------------ +// prettier-ignore +type TFromMappedResult< + R extends TMappedResult, + F extends boolean, +> = ( + TFromProperties +) +// prettier-ignore +function FromMappedResult< + R extends TMappedResult, + F extends boolean, +>(R: R, F: F): TFromMappedResult { + return FromProperties(R.properties, F) as never +} +// ------------------------------------------------------------------ +// OptionalFromMappedResult +// ------------------------------------------------------------------ +// prettier-ignore +export type TOptionalFromMappedResult< + R extends TMappedResult, + F extends boolean, + P extends TProperties = TFromMappedResult +> = ( + TMappedResult

+) +// prettier-ignore +export function OptionalFromMappedResult< + R extends TMappedResult, + F extends boolean, + P extends TProperties = TFromMappedResult +>(R: R, F: F): TMappedResult

{ + const P = FromMappedResult(R, F) as unknown as P + return MappedResult(P) +} diff --git a/src/type/optional/optional.ts b/src/type/optional/optional.ts new file mode 100644 index 000000000..09a8951d7 --- /dev/null +++ b/src/type/optional/optional.ts @@ -0,0 +1,82 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { CreateType } from '../create/type' +import type { TSchema } from '../schema/index' +import type { Ensure } from '../helpers/index' +import { OptionalKind } from '../symbols/index' +import { Discard } from '../discard/index' +import type { TMappedResult } from '../mapped/index' + +import { OptionalFromMappedResult, type TOptionalFromMappedResult } from './optional-from-mapped-result' +import { IsMappedResult } from '../guard/kind' +// ------------------------------------------------------------------ +// RemoveOptional +// ------------------------------------------------------------------ +type TRemoveOptional = T extends TOptional ? S : T +function RemoveOptional(schema: T) { + return CreateType(Discard(schema, [OptionalKind])) +} +// ------------------------------------------------------------------ +// AddOptional +// ------------------------------------------------------------------ +type TAddOptional = T extends TOptional ? TOptional : Ensure> +function AddOptional(schema: T) { + return CreateType({ ...schema, [OptionalKind]: 'Optional' }) +} +// prettier-ignore +export type TOptionalWithFlag = + F extends false + ? TRemoveOptional + : TAddOptional +// prettier-ignore +function OptionalWithFlag(schema: T, F: F) { + return ( + F === false + ? RemoveOptional(schema) + : AddOptional(schema) + ) +} +// ------------------------------------------------------------------ +// Optional +// ------------------------------------------------------------------ +export type TOptional = T & { [OptionalKind]: 'Optional' } + +/** `[Json]` Creates a Optional property */ +export function Optional(schema: T, enable: F): TOptionalFromMappedResult +/** `[Json]` Creates a Optional property */ +export function Optional(schema: T, enable: F): TOptionalWithFlag +/** `[Json]` Creates a Optional property */ +export function Optional(schema: T): TOptionalFromMappedResult +/** `[Json]` Creates a Optional property */ +export function Optional(schema: T): TOptionalWithFlag +/** `[Json]` Creates a Optional property */ +export function Optional(schema: TSchema, enable?: boolean): any { + const F = enable ?? true + return IsMappedResult(schema) ? OptionalFromMappedResult(schema, F) : OptionalWithFlag(schema, F) +} diff --git a/src/type/parameters/index.ts b/src/type/parameters/index.ts new file mode 100644 index 000000000..60fa18ad5 --- /dev/null +++ b/src/type/parameters/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './parameters' diff --git a/src/type/parameters/parameters.ts b/src/type/parameters/parameters.ts new file mode 100644 index 000000000..02ec79500 --- /dev/null +++ b/src/type/parameters/parameters.ts @@ -0,0 +1,47 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { TSchema, SchemaOptions } from '../schema/index' +import type { TFunction } from '../function/index' +import { Tuple, type TTuple } from '../tuple/index' +import { Never, type TNever } from '../never/index' +import * as KindGuard from '../guard/kind' + +// ------------------------------------------------------------------ +// Parameters +// ------------------------------------------------------------------ +// prettier-ignore +export type TParameters = ( + Type extends TFunction + ? TTuple + : TNever +) +/** `[JavaScript]` Extracts the Parameters from the given Function type */ +export function Parameters(schema: Type, options?: SchemaOptions): TParameters { + return (KindGuard.IsFunction(schema) ? Tuple(schema.parameters, options) : Never()) as never +} diff --git a/src/type/partial/index.ts b/src/type/partial/index.ts new file mode 100644 index 000000000..af260312a --- /dev/null +++ b/src/type/partial/index.ts @@ -0,0 +1,30 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './partial-from-mapped-result' +export * from './partial' diff --git a/src/type/partial/partial-from-mapped-result.ts b/src/type/partial/partial-from-mapped-result.ts new file mode 100644 index 000000000..65f6ba978 --- /dev/null +++ b/src/type/partial/partial-from-mapped-result.ts @@ -0,0 +1,85 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { SchemaOptions } from '../schema/index' +import type { Ensure, Evaluate } from '../helpers/index' +import type { TProperties } from '../object/index' +import { MappedResult, type TMappedResult } from '../mapped/index' +import { Partial, type TPartial } from './partial' +import { Clone } from '../clone/value' + +// ------------------------------------------------------------------ +// FromProperties +// ------------------------------------------------------------------ +// prettier-ignore +type TFromProperties< + P extends TProperties +> = ( + { [K2 in keyof P]: TPartial } +) +// prettier-ignore +function FromProperties< + P extends TProperties +>(K: P, options?: SchemaOptions): TFromProperties

{ + const Acc = {} as TProperties + for(const K2 of globalThis.Object.getOwnPropertyNames(K)) Acc[K2] = Partial(K[K2], Clone(options)) + return Acc as never +} +// ------------------------------------------------------------------ +// FromMappedResult +// ------------------------------------------------------------------ +// prettier-ignore +type TFromMappedResult< + R extends TMappedResult +> = ( + Evaluate> +) +// prettier-ignore +function FromMappedResult< + R extends TMappedResult +>(R: R, options?: SchemaOptions): TFromMappedResult { + return FromProperties(R.properties, options) as never +} +// ------------------------------------------------------------------ +// TPartialFromMappedResult +// ------------------------------------------------------------------ +// prettier-ignore +export type TPartialFromMappedResult< + R extends TMappedResult, + P extends TProperties = TFromMappedResult +> = ( + Ensure> +) +// prettier-ignore +export function PartialFromMappedResult< + R extends TMappedResult, + P extends TProperties = TFromMappedResult +>(R: R, options?: SchemaOptions): TMappedResult

{ + const P = FromMappedResult(R, options) + return MappedResult(P) as never +} diff --git a/src/type/partial/partial.ts b/src/type/partial/partial.ts new file mode 100644 index 000000000..1f1ac83ac --- /dev/null +++ b/src/type/partial/partial.ts @@ -0,0 +1,189 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { CreateType } from '../create/type' +import type { TSchema, SchemaOptions } from '../schema/index' +import type { Evaluate, Ensure } from '../helpers/index' +import type { TMappedResult } from '../mapped/index' +import { type TReadonlyOptional } from '../readonly-optional/index' +import { type TComputed, Computed } from '../computed/index' +import { type TOptional, Optional } from '../optional/index' +import { type TReadonly } from '../readonly/index' +import { type TRecursive } from '../recursive/index' +import { type TObject, type TProperties, Object } from '../object/index' +import { type TIntersect, Intersect } from '../intersect/index' +import { type TUnion, Union } from '../union/index' +import { type TRef, Ref } from '../ref/index' +import { type TBigInt } from '../bigint/index' +import { type TBoolean } from '../boolean/index' +import { type TInteger } from '../integer/index' +import { type TLiteral } from '../literal/index' +import { type TNull } from '../null/index' +import { type TNumber } from '../number/index' +import { type TString } from '../string/index' +import { type TSymbol } from '../symbol/index' +import { type TUndefined } from '../undefined/index' + +import { Discard } from '../discard/index' +import { TransformKind } from '../symbols/index' +import { PartialFromMappedResult, type TPartialFromMappedResult } from './partial-from-mapped-result' + +// ------------------------------------------------------------------ +// KindGuard +// ------------------------------------------------------------------ +import * as KindGuard from '../guard/kind' + +// ------------------------------------------------------------------ +// FromComputed +// ------------------------------------------------------------------ +// prettier-ignore +type TFromComputed = Ensure< + TComputed<'Partial', [TComputed]> +> +// prettier-ignore +function FromComputed(target: Target, parameters: Parameters): TFromComputed { + return Computed('Partial', [Computed(target, parameters)]) as never +} +// ------------------------------------------------------------------ +// FromRef +// ------------------------------------------------------------------ +// prettier-ignore +type TFromRef = Ensure< + TComputed<'Partial', [TRef]> +> +// prettier-ignore +function FromRef($ref: Ref): TFromRef { + return Computed('Partial', [Ref($ref)]) as never +} +// ------------------------------------------------------------------ +// FromProperties +// ------------------------------------------------------------------ +// prettier-ignore +type TFromProperties = Evaluate<{ + [K in keyof Properties]: + Properties[K] extends (TReadonlyOptional) ? TReadonlyOptional : + Properties[K] extends (TReadonly) ? TReadonlyOptional : + Properties[K] extends (TOptional) ? TOptional : + TOptional +}> +// prettier-ignore +function FromProperties(properties: Properties): TFromProperties { + const partialProperties = {} as TProperties + for(const K of globalThis.Object.getOwnPropertyNames(properties)) partialProperties[K] = Optional(properties[K]) + return partialProperties as never +} +// ------------------------------------------------------------------ +// FromObject +// ------------------------------------------------------------------ +// prettier-ignore +type TFromObject = Ensure +)>> +// prettier-ignore +function FromObject(type: Type): TFromObject { + const options = Discard(type, [TransformKind, '$id', 'required', 'properties']) + const properties = FromProperties(type['properties']) + return Object(properties, options) as never +} +// ------------------------------------------------------------------ +// FromRest +// ------------------------------------------------------------------ +// prettier-ignore +type TFromRest = ( + Types extends [infer L extends TSchema, ...infer R extends TSchema[]] + ? TFromRest]> + : Result +) +// prettier-ignore +function FromRest(types: [...Types]): TFromRest { + return types.map(type => PartialResolve(type)) as never +} +// ------------------------------------------------------------------ +// PartialResolve +// ------------------------------------------------------------------ +// prettier-ignore +function PartialResolve(type: Type): TPartial { + return ( + // Mappable + KindGuard.IsComputed(type) ? FromComputed(type.target, type.parameters) : + KindGuard.IsRef(type) ? FromRef(type.$ref) : + KindGuard.IsIntersect(type) ? Intersect(FromRest(type.allOf)) : + KindGuard.IsUnion(type) ? Union(FromRest(type.anyOf)) : + KindGuard.IsObject(type) ? FromObject(type) : + // Intrinsic + KindGuard.IsBigInt(type) ? type : + KindGuard.IsBoolean(type) ? type : + KindGuard.IsInteger(type) ? type : + KindGuard.IsLiteral(type) ? type : + KindGuard.IsNull(type) ? type : + KindGuard.IsNumber(type) ? type : + KindGuard.IsString(type) ? type : + KindGuard.IsSymbol(type) ? type : + KindGuard.IsUndefined(type) ? type : + // Passthrough + Object({}) + ) as never +} +// ------------------------------------------------------------------ +// TPartial +// ------------------------------------------------------------------ +// prettier-ignore +export type TPartial = ( + // Mappable + Type extends TRecursive ? TRecursive> : + Type extends TComputed ? TFromComputed : + Type extends TRef ? TFromRef : + Type extends TIntersect ? TIntersect> : + Type extends TUnion ? TUnion> : + Type extends TObject ? TFromObject> : + // Intrinsic + Type extends TBigInt ? Type : + Type extends TBoolean ? Type : + Type extends TInteger ? Type : + Type extends TLiteral ? Type : + Type extends TNull ? Type : + Type extends TNumber ? Type : + Type extends TString ? Type : + Type extends TSymbol ? Type : + Type extends TUndefined ? Type : + // Passthrough + TObject<{}> +) +/** `[Json]` Constructs a type where all properties are optional */ +export function Partial(type: MappedResult, options?: SchemaOptions): TPartialFromMappedResult +/** `[Json]` Constructs a type where all properties are optional */ +export function Partial(type: Type, options?: SchemaOptions): TPartial +/** `[Json]` Constructs a type where all properties are optional */ +export function Partial(type: TSchema, options?: SchemaOptions): unknown { + if (KindGuard.IsMappedResult(type)) { + return PartialFromMappedResult(type, options) + } else { + // special: mapping types require overridable options + return CreateType({ ...PartialResolve(type), ...options }) + } +} diff --git a/src/type/patterns/index.ts b/src/type/patterns/index.ts new file mode 100644 index 000000000..619eb64e1 --- /dev/null +++ b/src/type/patterns/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './patterns' diff --git a/src/type/patterns/patterns.ts b/src/type/patterns/patterns.ts new file mode 100644 index 000000000..bb44c60fb --- /dev/null +++ b/src/type/patterns/patterns.ts @@ -0,0 +1,36 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export const PatternBoolean = '(true|false)' +export const PatternNumber = '(0|[1-9][0-9]*)' +export const PatternString = '(.*)' +export const PatternNever = '(?!.*)' +export const PatternBooleanExact = `^${PatternBoolean}$` +export const PatternNumberExact = `^${PatternNumber}$` +export const PatternStringExact = `^${PatternString}$` +export const PatternNeverExact = `^${PatternNever}$` diff --git a/src/type/pick/index.ts b/src/type/pick/index.ts new file mode 100644 index 000000000..8e659b8f4 --- /dev/null +++ b/src/type/pick/index.ts @@ -0,0 +1,31 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './pick-from-mapped-key' +export * from './pick-from-mapped-result' +export * from './pick' diff --git a/src/type/pick/pick-from-mapped-key.ts b/src/type/pick/pick-from-mapped-key.ts new file mode 100644 index 000000000..df17792e5 --- /dev/null +++ b/src/type/pick/pick-from-mapped-key.ts @@ -0,0 +1,89 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { TSchema, SchemaOptions } from '../schema/index' +import type { TProperties } from '../object/index' +import { MappedResult, type TMappedResult, type TMappedKey } from '../mapped/index' +import { Pick, type TPick } from './pick' +import { Clone } from '../clone/value' + +// ------------------------------------------------------------------ +// FromPropertyKey +// ------------------------------------------------------------------ +// prettier-ignore +type TFromPropertyKey = { + [_ in Key]: TPick +} +// prettier-ignore +function FromPropertyKey(type: Type, key: Key, options?: SchemaOptions): TFromPropertyKey { + return { + [key]: Pick(type, [key], Clone(options)) + } as never +} +// ------------------------------------------------------------------ +// FromPropertyKeys +// ------------------------------------------------------------------ +// prettier-ignore +type TFromPropertyKeys = ( + PropertyKeys extends [infer LeftKey extends PropertyKey, ...infer RightKeys extends PropertyKey[]] + ? TFromPropertyKeys> + : Result +) +// prettier-ignore +function FromPropertyKeys(type: Type, propertyKeys: [...PropertyKeys], options?: SchemaOptions): TFromPropertyKeys { + return propertyKeys.reduce((result, leftKey) => { + return { ...result, ...FromPropertyKey(type, leftKey, options) } + }, {} as TProperties) as never +} +// ------------------------------------------------------------------ +// FromMappedKey +// ------------------------------------------------------------------ +// prettier-ignore +type TFromMappedKey = ( + TFromPropertyKeys +) +// prettier-ignore +function FromMappedKey(type: Type, mappedKey: MappedKey, options?: SchemaOptions): TFromMappedKey { + return FromPropertyKeys(type, mappedKey.keys, options) as never +} +// ------------------------------------------------------------------ +// PickFromMappedKey +// ------------------------------------------------------------------ +// prettier-ignore +export type TPickFromMappedKey +> = ( + TMappedResult +) +// prettier-ignore +export function PickFromMappedKey +>(type: Type, mappedKey: MappedKey, options?: SchemaOptions): TMappedResult { + const properties = FromMappedKey(type, mappedKey, options) + return MappedResult(properties) as never +} diff --git a/src/type/pick/pick-from-mapped-result.ts b/src/type/pick/pick-from-mapped-result.ts new file mode 100644 index 000000000..75b9dbd04 --- /dev/null +++ b/src/type/pick/pick-from-mapped-result.ts @@ -0,0 +1,77 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { SchemaOptions } from '../schema/index' +import type { Ensure, Evaluate } from '../helpers/index' +import type { TProperties } from '../object/index' +import { MappedResult, type TMappedResult } from '../mapped/index' +import { Pick, type TPick } from './pick' +import { Clone } from '../clone/value' + +// ------------------------------------------------------------------ +// FromProperties +// ------------------------------------------------------------------ +// prettier-ignore +type TFromProperties = ( + { [K2 in keyof Properties]: TPick } +) +// prettier-ignore +function FromProperties(properties: Properties, propertyKeys: [...PropertyKeys], options?: SchemaOptions): TFromProperties { + const result = {} as TProperties + for(const K2 of globalThis.Object.getOwnPropertyNames(properties)) result[K2] = Pick(properties[K2], propertyKeys, Clone(options)) + return result as never +} +// ------------------------------------------------------------------ +// FromMappedResult +// ------------------------------------------------------------------ +// prettier-ignore +type TFromMappedResult = ( + Evaluate> +) +// prettier-ignore +function FromMappedResult(mappedResult: MappedResult, propertyKeys: [...PropertyKeys], options?: SchemaOptions): TFromMappedResult { + return FromProperties(mappedResult.properties, propertyKeys, options) as never +} +// ------------------------------------------------------------------ +// PickFromMappedResult +// ------------------------------------------------------------------ +// prettier-ignore +export type TPickFromMappedResult +> = ( + Ensure> +) +// prettier-ignore +export function PickFromMappedResult< + MappedResult extends TMappedResult, + PropertyKeys extends PropertyKey[], + Properties extends TProperties = TFromMappedResult +>(mappedResult: MappedResult, propertyKeys: [...PropertyKeys], options?: SchemaOptions): TMappedResult { + const properties = FromMappedResult(mappedResult, propertyKeys, options) + return MappedResult(properties) as never +} diff --git a/src/type/pick/pick.ts b/src/type/pick/pick.ts new file mode 100644 index 000000000..b7042fb69 --- /dev/null +++ b/src/type/pick/pick.ts @@ -0,0 +1,188 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { CreateType } from '../create/type' +import { Discard } from '../discard/discard' +import type { TSchema, SchemaOptions } from '../schema/index' +import type { TupleToUnion, Evaluate, Ensure } from '../helpers/index' +import { type TRecursive } from '../recursive/index' +import { Computed, type TComputed } from '../computed/index' +import { Intersect, type TIntersect } from '../intersect/index' +import { Literal, type TLiteral, type TLiteralValue } from '../literal/index' +import { Object, type TObject, type TProperties, type TPropertyKey } from '../object/index' +import { Union, type TUnion } from '../union/index' +import { type TMappedKey, type TMappedResult } from '../mapped/index' +import { type TRef } from '../ref/index' +import { IndexPropertyKeys, type TIndexPropertyKeys } from '../indexed/index' +import { TransformKind } from '../symbols/symbols' + +// ------------------------------------------------------------------ +// Guards +// ------------------------------------------------------------------ +import { IsMappedKey, IsMappedResult, IsIntersect, IsUnion, IsObject, IsSchema, IsLiteralValue, IsRef } from '../guard/kind' +import { IsArray as IsArrayValue } from '../guard/value' + +// ------------------------------------------------------------------ +// Infrastructure +// ------------------------------------------------------------------ +import { PickFromMappedKey, type TPickFromMappedKey } from './pick-from-mapped-key' +import { PickFromMappedResult, type TPickFromMappedResult } from './pick-from-mapped-result' + +// ------------------------------------------------------------------ +// FromIntersect +// ------------------------------------------------------------------ +// prettier-ignore +type TFromIntersect = + Types extends [infer L extends TSchema, ...infer R extends TSchema[]] + ? TFromIntersect]> + : Result +function FromIntersect(types: Types, propertyKeys: PropertyKeys): TFromIntersect { + return types.map((type) => PickResolve(type, propertyKeys)) as never +} +// ------------------------------------------------------------------ +// FromUnion +// ------------------------------------------------------------------ +// prettier-ignore +type TFromUnion = + Types extends [infer L extends TSchema, ...infer R extends TSchema[]] + ? TFromUnion]> + : Result +// prettier-ignore +function FromUnion(types: Types, propertyKeys: PropertyKeys): TFromUnion { + return types.map((type) => PickResolve(type, propertyKeys)) as never +} +// ------------------------------------------------------------------ +// FromProperties +// ------------------------------------------------------------------ +// prettier-ignore +type TFromProperties> = ( + Evaluate> +) +// prettier-ignore +function FromProperties(properties: Properties, propertyKeys: PropertyKeys): TFromProperties { + const result = {} as TProperties + for(const K2 of propertyKeys) if(K2 in properties) result[K2 as TPropertyKey] = properties[K2 as keyof Properties] + return result as never +} +// ------------------------------------------------------------------ +// FromObject +// ------------------------------------------------------------------ +// prettier-ignore +type TFromObject = Ensure +)>> +// prettier-ignore +function FromObject(T: T, K: K): TFromObject { + const options = Discard(T, [TransformKind, '$id', 'required', 'properties']) + const properties = FromProperties(T['properties'], K) + return Object(properties, options) as never +} +// ------------------------------------------------------------------ +// UnionFromPropertyKeys +// ------------------------------------------------------------------ +// prettier-ignore +type TUnionFromPropertyKeys = ( + PropertyKeys extends [infer Key extends PropertyKey, ...infer Rest extends PropertyKey[]] + ? Key extends TLiteralValue + ? TUnionFromPropertyKeys]> + : TUnionFromPropertyKeys + : TUnion +) +// prettier-ignore +function UnionFromPropertyKeys(propertyKeys: PropertyKeys): TUnionFromPropertyKeys { + const result = propertyKeys.reduce((result, key) => IsLiteralValue(key) ? [...result, Literal(key)]: result, [] as TLiteral[]) + return Union(result) as never +} +// ------------------------------------------------------------------ +// TPickResolve +// ------------------------------------------------------------------ +// prettier-ignore +export type TPickResolve = ( + Properties extends TRecursive ? TRecursive> : + Properties extends TIntersect ? TIntersect> : + Properties extends TUnion ? TUnion> : + Properties extends TObject ? TFromObject, PropertyKeys> : + TObject<{}> +) +// prettier-ignore +function PickResolve(properties: Properties, propertyKeys: [...PropertyKeys]): TPickResolve { + return ( + IsIntersect(properties) ? Intersect(FromIntersect(properties.allOf, propertyKeys)) : + IsUnion(properties) ? Union(FromUnion(properties.anyOf, propertyKeys)) : + IsObject(properties) ? FromObject(properties, propertyKeys) : + Object({}) + ) as never +} +// ------------------------------------------------------------------ +// TPick +// +// This mapping logic is to overly complex because of the decision +// to use PropertyKey[] as the default selector. The PropertyKey[] +// did make TMapped types simpler to implement, but a non-TSchema +// selector makes supporting TComputed awkward as it requires +// generalization via TSchema. This type should be reimplemented +// in the next major revision to support TSchema as the primary +// selector. +// +// ------------------------------------------------------------------ +// prettier-ignore (do not export this type) +type TResolvePropertyKeys = Key extends TSchema ? TIndexPropertyKeys : Key +// prettier-ignore (do not export this type) +type TResolveTypeKey = Key extends PropertyKey[] ? TUnionFromPropertyKeys : Key +// prettier-ignore +export type TPick = ( + Type extends TMappedResult ? TPickFromMappedResult> : + Key extends TMappedKey ? TPickFromMappedKey : + [IsTypeRef, IsKeyRef] extends [true, true] ? TComputed<'Pick', [Type, TResolveTypeKey]> : + [IsTypeRef, IsKeyRef] extends [false, true] ? TComputed<'Pick', [Type, TResolveTypeKey]> : + [IsTypeRef, IsKeyRef] extends [true, false] ? TComputed<'Pick', [Type, TResolveTypeKey]> : + TPickResolve> +) +/** `[Json]` Constructs a type whose keys are picked from the given type */ +export function Pick(type: Type, key: readonly [...Key], options?: SchemaOptions): TPick +/** `[Json]` Constructs a type whose keys are picked from the given type */ +export function Pick(type: Type, key: Key, options?: SchemaOptions): TPick +/** `[Json]` Constructs a type whose keys are picked from the given type */ +// prettier-ignore +export function Pick(type: any, key: any, options?: SchemaOptions): any { + const typeKey: TSchema = IsArrayValue(key) ? UnionFromPropertyKeys(key as PropertyKey[]) : key + const propertyKeys: PropertyKey[] = IsSchema(key) ? IndexPropertyKeys(key) : key + const isTypeRef: boolean = IsRef(type) + const isKeyRef: boolean = IsRef(key) + return ( + IsMappedResult(type) ? PickFromMappedResult(type, propertyKeys, options) : + IsMappedKey(key) ? PickFromMappedKey(type, key, options) : + (isTypeRef && isKeyRef) ? Computed('Pick', [type, typeKey], options) : + (!isTypeRef && isKeyRef) ? Computed('Pick', [type, typeKey], options) : + (isTypeRef && !isKeyRef) ? Computed('Pick', [type, typeKey], options) : + CreateType({ ...PickResolve(type, propertyKeys), ...options }) + ) as never +} diff --git a/src/type/promise/index.ts b/src/type/promise/index.ts new file mode 100644 index 000000000..1436cfa71 --- /dev/null +++ b/src/type/promise/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './promise' diff --git a/src/type/promise/promise.ts b/src/type/promise/promise.ts new file mode 100644 index 000000000..cb2311c61 --- /dev/null +++ b/src/type/promise/promise.ts @@ -0,0 +1,43 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { CreateType } from '../create/type' +import type { TSchema, SchemaOptions } from '../schema/index' +import type { Static } from '../static/index' +import { Kind } from '../symbols/index' + +export interface TPromise extends TSchema { + [Kind]: 'Promise' + static: Promise> + type: 'Promise' + item: TSchema +} +/** `[JavaScript]` Creates a Promise type */ +export function Promise(item: T, options?: SchemaOptions): TPromise { + return CreateType({ [Kind]: 'Promise', type: 'Promise', item }, options) as never +} diff --git a/src/type/readonly-optional/index.ts b/src/type/readonly-optional/index.ts new file mode 100644 index 000000000..8e2481eff --- /dev/null +++ b/src/type/readonly-optional/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './readonly-optional' diff --git a/src/type/readonly-optional/readonly-optional.ts b/src/type/readonly-optional/readonly-optional.ts new file mode 100644 index 000000000..d53f74b50 --- /dev/null +++ b/src/type/readonly-optional/readonly-optional.ts @@ -0,0 +1,38 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { TSchema } from '../schema/index' +import { type TReadonly, Readonly } from '../readonly/index' +import { type TOptional, Optional } from '../optional/index' + +export type TReadonlyOptional = TOptional & TReadonly + +/** `[Json]` Creates a Readonly and Optional property */ +export function ReadonlyOptional(schema: T): TReadonly> { + return Readonly(Optional(schema)) as never +} diff --git a/src/type/readonly/index.ts b/src/type/readonly/index.ts new file mode 100644 index 000000000..356f84b10 --- /dev/null +++ b/src/type/readonly/index.ts @@ -0,0 +1,30 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './readonly-from-mapped-result' +export * from './readonly' diff --git a/src/type/readonly/readonly-from-mapped-result.ts b/src/type/readonly/readonly-from-mapped-result.ts new file mode 100644 index 000000000..62134e8e9 --- /dev/null +++ b/src/type/readonly/readonly-from-mapped-result.ts @@ -0,0 +1,88 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { TProperties } from '../object/index' +import { MappedResult, type TMappedResult } from '../mapped/index' +import { Readonly, type TReadonlyWithFlag } from './readonly' + +// ------------------------------------------------------------------ +// FromProperties +// ------------------------------------------------------------------ +// prettier-ignore +type TFromProperties< + P extends TProperties, + F extends boolean, +> = ( + { [K2 in keyof P]: TReadonlyWithFlag } +) +// prettier-ignore +function FromProperties< + P extends TProperties, + F extends boolean, +>(K: P, F: F): TFromProperties { + const Acc = {} as TProperties + for(const K2 of globalThis.Object.getOwnPropertyNames(K)) Acc[K2] = Readonly(K[K2], F) + return Acc as never +} +// ------------------------------------------------------------------ +// FromMappedResult +// ------------------------------------------------------------------ +// prettier-ignore +type TFromMappedResult< + R extends TMappedResult, + F extends boolean, +> = ( + TFromProperties + ) +// prettier-ignore +function FromMappedResult< + R extends TMappedResult, + F extends boolean, +>(R: R, F: F): TFromMappedResult { + return FromProperties(R.properties, F) as never +} +// ------------------------------------------------------------------ +// ReadonlyFromMappedResult +// ------------------------------------------------------------------ +// prettier-ignore +export type TReadonlyFromMappedResult< + R extends TMappedResult, + F extends boolean, + P extends TProperties = TFromMappedResult +> = ( + TMappedResult

+) +// prettier-ignore +export function ReadonlyFromMappedResult< + R extends TMappedResult, + F extends boolean, + P extends TProperties = TFromMappedResult +>(R: R, F: F): TMappedResult

{ + const P = FromMappedResult(R, F) + return MappedResult(P) as never +} diff --git a/src/type/readonly/readonly.ts b/src/type/readonly/readonly.ts new file mode 100644 index 000000000..bf77263ed --- /dev/null +++ b/src/type/readonly/readonly.ts @@ -0,0 +1,82 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { CreateType } from '../create/type' +import type { TSchema } from '../schema/index' +import type { Ensure } from '../helpers/index' +import { ReadonlyKind } from '../symbols/index' +import { Discard } from '../discard/index' +import type { TMappedResult } from '../mapped/index' + +import { ReadonlyFromMappedResult, type TReadonlyFromMappedResult } from './readonly-from-mapped-result' +import { IsMappedResult } from '../guard/kind' +// ------------------------------------------------------------------ +// RemoveReadonly +// ------------------------------------------------------------------ +type TRemoveReadonly = T extends TReadonly ? S : T +function RemoveReadonly(schema: T) { + return CreateType(Discard(schema, [ReadonlyKind])) +} +// ------------------------------------------------------------------ +// AddReadonly +// ------------------------------------------------------------------ +type TAddReadonly = T extends TReadonly ? TReadonly : Ensure> +function AddReadonly(schema: T) { + return CreateType({ ...schema, [ReadonlyKind]: 'Readonly' }) +} +// prettier-ignore +export type TReadonlyWithFlag = + F extends false + ? TRemoveReadonly + : TAddReadonly +// prettier-ignore +function ReadonlyWithFlag(schema: T, F: F) { + return ( + F === false + ? RemoveReadonly(schema) + : AddReadonly(schema) + ) +} +// ------------------------------------------------------------------ +// TReadonly +// ------------------------------------------------------------------ +export type TReadonly = T & { [ReadonlyKind]: 'Readonly' } + +/** `[Json]` Creates a Readonly property */ +export function Readonly(schema: T, enable: F): TReadonlyFromMappedResult +/** `[Json]` Creates a Readonly property */ +export function Readonly(schema: T, enable: F): TReadonlyWithFlag +/** `[Json]` Creates a Readonly property */ +export function Readonly(schema: T): TReadonlyFromMappedResult +/** `[Json]` Creates a Readonly property */ +export function Readonly(schema: T): TReadonlyWithFlag +/** `[Json]` Creates a Readonly property */ +export function Readonly(schema: TSchema, enable?: boolean): any { + const F = enable ?? true + return IsMappedResult(schema) ? ReadonlyFromMappedResult(schema, F) : ReadonlyWithFlag(schema, F) +} diff --git a/src/type/record/index.ts b/src/type/record/index.ts new file mode 100644 index 000000000..7ba836747 --- /dev/null +++ b/src/type/record/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './record' diff --git a/src/type/record/record.ts b/src/type/record/record.ts new file mode 100644 index 000000000..c118c8ab7 --- /dev/null +++ b/src/type/record/record.ts @@ -0,0 +1,313 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { CreateType } from '../create/type' +import { Kind, Hint } from '../symbols/index' +import type { TSchema } from '../schema/index' +import type { Static } from '../static/index' +import type { Evaluate, Ensure, Assert } from '../helpers/index' +import { type TAny } from '../any/index' +import { type TBoolean } from '../boolean/index' +import { type TEnumRecord, type TEnum } from '../enum/index' +import { type TInteger } from '../integer/index' +import { type TLiteral, type TLiteralValue } from '../literal/index' +import { type TNever, Never } from '../never/index' +import { type TNumber, Number } from '../number/index' +import { type TObject, type TProperties, type TAdditionalProperties, type ObjectOptions, Object } from '../object/index' +import { type TRegExp } from '../regexp/index' +import { type TString, String } from '../string/index' +import { type TUnion, Union } from '../union/index' + +import { IsTemplateLiteralFinite, TIsTemplateLiteralFinite, type TTemplateLiteral } from '../template-literal/index' +import { PatternStringExact, PatternNumberExact, PatternNeverExact } from '../patterns/index' +import { IndexPropertyKeys } from '../indexed/index' + +// ------------------------------------------------------------------ +// ValueGuard +// ------------------------------------------------------------------ +import { IsUndefined } from '../guard/value' +// ------------------------------------------------------------------ +// TypeGuard +// ------------------------------------------------------------------ +import { IsInteger, IsLiteral, IsAny, IsBoolean, IsNever, IsNumber, IsString, IsRegExp, IsTemplateLiteral, IsUnion, IsRef, IsComputed } from '../guard/kind' + +// ------------------------------------------------------------------ +// RecordCreateFromPattern +// ------------------------------------------------------------------ +// prettier-ignore +function RecordCreateFromPattern(pattern: string, T: TSchema, options: ObjectOptions): TRecord { + return CreateType({ [Kind]: 'Record', type: 'object', patternProperties: { [pattern]: T } }, options) as never +} +// ------------------------------------------------------------------ +// RecordCreateFromKeys +// ------------------------------------------------------------------ +// prettier-ignore +function RecordCreateFromKeys(K: string[], T: TSchema, options: ObjectOptions): TObject { + const result = {} as TProperties + for(const K2 of K) result[K2] = T + return Object(result, { ...options, [Hint]: 'Record' }) +} +// ------------------------------------------------------------------ +// FromTemplateLiteralKey (Fast Inference) +// ------------------------------------------------------------------ +// prettier-ignore +type TFromTemplateLiteralKeyInfinite = Ensure> +// prettier-ignore +type TFromTemplateLiteralKeyFinite> = ( + Ensure>> +) +// prettier-ignore +type TFromTemplateLiteralKey = TIsTemplateLiteralFinite extends false + ? TFromTemplateLiteralKeyInfinite + : TFromTemplateLiteralKeyFinite +// prettier-ignore +function FromTemplateLiteralKey(K: Key, T: Type, options: ObjectOptions): TFromTemplateLiteralKey { + return ( + IsTemplateLiteralFinite(K) + ? RecordCreateFromKeys(IndexPropertyKeys(K), T, options) + : RecordCreateFromPattern(K.pattern, T, options) + ) as never +} +// ------------------------------------------------------------------ +// FromEnumKey (Special Case) +// ------------------------------------------------------------------ +// prettier-ignore +type TFromEnumKey, Type extends TSchema> = Ensure> +// ------------------------------------------------------------------ +// FromUnionKey +// ------------------------------------------------------------------ +// prettier-ignore +type TFromUnionKeyLiteralString, Type extends TSchema> = { [_ in Key['const']]: Type } +// prettier-ignore +type TFromUnionKeyLiteralNumber, Type extends TSchema> = { [_ in Key['const']]: Type } +// prettier-ignore +type TFromUnionKeyVariants = + Keys extends [infer Left extends TSchema, ...infer Right extends TSchema[]] ? ( + Left extends TUnion ? TFromUnionKeyVariants> : + Left extends TLiteral ? TFromUnionKeyVariants> : + Left extends TLiteral ? TFromUnionKeyVariants> : + {}) : Result +// prettier-ignore +type TFromUnionKey> = ( + Ensure>> +) +// prettier-ignore +function FromUnionKey(key: Key, type: Type, options: ObjectOptions): TFromUnionKey { + return RecordCreateFromKeys(IndexPropertyKeys(Union(key)), type, options) as never +} +// ------------------------------------------------------------------ +// FromLiteralKey +// ------------------------------------------------------------------ +// prettier-ignore +type TFromLiteralKey = ( + Ensure]: Type }>> +) +// prettier-ignore +function FromLiteralKey(key: Key, type: Type, options: ObjectOptions): TFromLiteralKey { + return RecordCreateFromKeys([(key as string).toString()], type, options) as never +} +// ------------------------------------------------------------------ +// TFromRegExpKey +// ------------------------------------------------------------------ +// prettier-ignore +type TFromRegExpKey<_Key extends TRegExp, Type extends TSchema> = ( + Ensure> +) +// prettier-ignore +function FromRegExpKey(key: Key, type: Type, options: ObjectOptions): TFromRegExpKey { + return RecordCreateFromPattern(key.source, type, options) as never +} +// ------------------------------------------------------------------ +// FromStringKey +// ------------------------------------------------------------------ +// prettier-ignore +type TFromStringKey<_Key extends TString, Type extends TSchema> = ( + Ensure> +) +// prettier-ignore +function FromStringKey(key: Key, type: Type, options: ObjectOptions): TFromStringKey { + const pattern = IsUndefined(key.pattern) ? PatternStringExact : key.pattern + return RecordCreateFromPattern(pattern, type, options) as never +} +// ------------------------------------------------------------------ +// FromAnyKey +// ------------------------------------------------------------------ +// prettier-ignore +type TFromAnyKey<_Key extends TAny, Type extends TSchema> = ( + Ensure> +) +// prettier-ignore +function FromAnyKey(_: Key, type: Type, options: ObjectOptions): TFromAnyKey { + return RecordCreateFromPattern(PatternStringExact, type, options) as never +} +// ------------------------------------------------------------------ +// FromNeverKey +// ------------------------------------------------------------------ +// prettier-ignore +type TFromNeverKey<_Key extends TNever, Type extends TSchema> = ( + Ensure> +) +// prettier-ignore +function FromNeverKey(_key: Key, type: Type, options: ObjectOptions): TFromNeverKey { + return RecordCreateFromPattern(PatternNeverExact, type, options) as never +} +// ------------------------------------------------------------------ +// TromBooleanKey +// ------------------------------------------------------------------ +// prettier-ignore +type TFromBooleanKey<_Key extends TBoolean, Type extends TSchema> = ( + Ensure> +) +// prettier-ignore +function FromBooleanKey(_key: Key, type: Type, options: ObjectOptions): TFromBooleanKey { + return Object({ true: type, false: type }, options) +} +// ------------------------------------------------------------------ +// FromIntegerKey +// ------------------------------------------------------------------ +// prettier-ignore +type TFromIntegerKey<_Key extends TSchema, Type extends TSchema> = ( + Ensure> +) +// prettier-ignore +function FromIntegerKey(_key: Key, type: Type, options: ObjectOptions): TFromIntegerKey { + return RecordCreateFromPattern(PatternNumberExact, type, options) as never +} +// ------------------------------------------------------------------ +// FromNumberKey +// ------------------------------------------------------------------ +// prettier-ignore +type TFromNumberKey<_Key extends TSchema, Type extends TSchema> = ( + Ensure> +) +// prettier-ignore +function FromNumberKey(_: Key, type: Type, options: ObjectOptions): TFromNumberKey { + return RecordCreateFromPattern(PatternNumberExact, type, options) as never +} +// ------------------------------------------------------------------ +// TRecord +// ------------------------------------------------------------------ +// prettier-ignore +type RecordStatic = ( + // Note: We would return a Record here, but recursive Record types will + // break when T is self recursive. We can mitigate this by using a Mapped instead. + Evaluate<{ [_ in Assert, PropertyKey>]: Static }> +) +// prettier-ignore +export interface TRecord extends TSchema { + [Kind]: 'Record' + static: RecordStatic + type: 'object' + patternProperties: { [pattern: string]: Type } + additionalProperties: TAdditionalProperties +} +// ------------------------------------------------------------------ +// TRecordOrObject +// ------------------------------------------------------------------ +// prettier-ignore +export type TRecordOrObject = ( + Key extends TTemplateLiteral ? TFromTemplateLiteralKey : + Key extends TEnum ? TFromEnumKey : // (Special: Ensure resolve Enum before Union) + Key extends TUnion ? TFromUnionKey : + Key extends TLiteral ? TFromLiteralKey : + Key extends TBoolean ? TFromBooleanKey : + Key extends TInteger ? TFromIntegerKey : + Key extends TNumber ? TFromNumberKey : + Key extends TRegExp ? TFromRegExpKey : + Key extends TString ? TFromStringKey : + Key extends TAny ? TFromAnyKey : + Key extends TNever ? TFromNeverKey : + TNever +) +// ------------------------------------------------------------------ +// TRecordOrObject +// ------------------------------------------------------------------ +/** `[Json]` Creates a Record type */ +export function Record(key: Key, type: Type, options: ObjectOptions = {}): TRecordOrObject { + // prettier-ignore + return ( + IsUnion(key) ? FromUnionKey(key.anyOf, type, options) : + IsTemplateLiteral(key) ? FromTemplateLiteralKey(key, type, options) : + IsLiteral(key) ? FromLiteralKey(key.const, type, options) : + IsBoolean(key) ? FromBooleanKey(key, type, options) : + IsInteger(key) ? FromIntegerKey(key, type, options) : + IsNumber(key) ? FromNumberKey(key, type, options) : + IsRegExp(key) ? FromRegExpKey(key, type, options) : + IsString(key) ? FromStringKey(key, type, options) : + IsAny(key) ? FromAnyKey(key, type, options) : + IsNever(key) ? FromNeverKey(key, type, options) : + Never(options) + ) as never +} + +// ------------------------------------------------------------------ +// Record Utilities +// ------------------------------------------------------------------ +/** Gets the Records Pattern */ +export function RecordPattern(record: TRecord): string { + return globalThis.Object.getOwnPropertyNames(record.patternProperties)[0] +} +// ------------------------------------------------------------------ +// RecordKey +// ------------------------------------------------------------------ +/** Gets the Records Key Type */ +// prettier-ignore +export type TRecordKey ? ( + Key extends TNumber ? TNumber : + Key extends TString ? TString : + TString + ) : TString +> = Result +/** Gets the Records Key Type */ +// prettier-ignore +export function RecordKey(type: Type): TRecordKey { + const pattern = RecordPattern(type) + return ( + pattern === PatternStringExact ? String() : + pattern === PatternNumberExact ? Number() : + String({ pattern }) + ) as never +} +// ------------------------------------------------------------------ +// RecordValue +// ------------------------------------------------------------------ +/** Gets a Record Value Type */ +// prettier-ignore +export type TRecordValue + ? Value + : TNever + ) +> = Result +/** Gets a Record Value Type */ +// prettier-ignore +export function RecordValue(type: Type): TRecordValue { + return type.patternProperties[RecordPattern(type)] as never +} diff --git a/src/type/recursive/index.ts b/src/type/recursive/index.ts new file mode 100644 index 000000000..b1e22c649 --- /dev/null +++ b/src/type/recursive/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './recursive' diff --git a/src/type/recursive/recursive.ts b/src/type/recursive/recursive.ts new file mode 100644 index 000000000..35f36253a --- /dev/null +++ b/src/type/recursive/recursive.ts @@ -0,0 +1,64 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { TSchema, SchemaOptions } from '../schema/index' +import { CloneType } from '../clone/type' +import { CreateType } from '../create/type' +import { IsUndefined } from '../guard/value' +import { Kind, Hint } from '../symbols/index' +import { Static } from '../static/index' + +// ------------------------------------------------------------------ +// TThis +// ------------------------------------------------------------------ +export interface TThis extends TSchema { + [Kind]: 'This' + static: this['params'][0] + $ref: string +} +// ------------------------------------------------------------------ +// RecursiveStatic +// ------------------------------------------------------------------ +type RecursiveStatic = Static]> +// ------------------------------------------------------------------ +// TRecursive +// ------------------------------------------------------------------ +export interface TRecursive extends TSchema { + [Hint]: 'Recursive' + static: RecursiveStatic +} +// Auto Tracked For Recursive Types without ID's +let Ordinal = 0 +/** `[Json]` Creates a Recursive type */ +export function Recursive(callback: (thisType: TThis) => T, options: SchemaOptions = {}): TRecursive { + if (IsUndefined(options.$id)) (options as any).$id = `T${Ordinal++}` + const thisType = CloneType(callback({ [Kind]: 'This', $ref: `${options.$id}` } as any)) + thisType.$id = options.$id + // prettier-ignore + return CreateType({ [Hint]: 'Recursive', ...thisType }, options) as never +} diff --git a/src/type/ref/index.ts b/src/type/ref/index.ts new file mode 100644 index 000000000..6daaa62b3 --- /dev/null +++ b/src/type/ref/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './ref' diff --git a/src/type/ref/ref.ts b/src/type/ref/ref.ts new file mode 100644 index 000000000..dd4f37b21 --- /dev/null +++ b/src/type/ref/ref.ts @@ -0,0 +1,83 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { TSchema, SchemaOptions } from '../schema/index' +import { TypeBoxError } from '../error/index' +import { CreateType } from '../create/type' +import { Kind } from '../symbols/index' +import { TUnsafe } from '../unsafe/index' +import { Static } from '../static/index' + +// ------------------------------------------------------------------ +// TRef +// ------------------------------------------------------------------ +export interface TRef extends TSchema { + [Kind]: 'Ref' + static: unknown + $ref: Ref +} + +export type TRefUnsafe = TUnsafe> + +/** `[Json]` Creates a Ref type.*/ +export function Ref($ref: Ref, options?: SchemaOptions): TRef +/** + * @deprecated `[Json]` Creates a Ref type. This signature was deprecated in 0.34.0 where Ref requires callers to pass + * a `string` value for the reference (and not a schema). + * + * To adhere to the 0.34.0 signature, Ref implementations should be updated to the following. + * + * ```typescript + * // pre-0.34.0 + * + * const T = Type.String({ $id: 'T' }) + * + * const R = Type.Ref(T) + * ``` + * should be changed to the following + * + * ```typescript + * // post-0.34.0 + * + * const T = Type.String({ $id: 'T' }) + * + * const R = Type.Unsafe>(Type.Ref('T')) + * ``` + * You can also create a generic function to replicate the pre-0.34.0 signature if required + * + * ```typescript + * const LegacyRef = (schema: T) => Type.Unsafe>(Type.Ref(schema.$id!)) + * ``` + */ +export function Ref(type: Type, options?: SchemaOptions): TRefUnsafe +/** `[Json]` Creates a Ref type. The referenced type must contain a $id */ +export function Ref(...args: any[]): unknown { + const [$ref, options] = typeof args[0] === 'string' ? [args[0], args[1]] : [args[0].$id, args[1]] + if (typeof $ref !== 'string') throw new TypeBoxError('Ref: $ref must be a string') + return CreateType({ [Kind]: 'Ref', $ref }, options) as never +} diff --git a/src/type/regexp/index.ts b/src/type/regexp/index.ts new file mode 100644 index 000000000..a2f473c62 --- /dev/null +++ b/src/type/regexp/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './regexp' diff --git a/src/type/regexp/regexp.ts b/src/type/regexp/regexp.ts new file mode 100644 index 000000000..7fc24ea27 --- /dev/null +++ b/src/type/regexp/regexp.ts @@ -0,0 +1,57 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { CreateType } from '../create/type' +import type { SchemaOptions } from '../schema/index' +import type { TSchema } from '../schema/index' +import { IsString } from '../guard/value' +import { Kind } from '../symbols/index' + +export interface RegExpOptions extends SchemaOptions { + /** The maximum length of the string */ + maxLength?: number + /** The minimum length of the string */ + minLength?: number +} +export interface TRegExp extends TSchema { + [Kind]: 'RegExp' + static: `${string}` + type: 'RegExp' + source: string + flags: string +} + +/** `[JavaScript]` Creates a RegExp type */ +export function RegExp(pattern: string, options?: RegExpOptions): TRegExp +/** `[JavaScript]` Creates a RegExp type */ +export function RegExp(regex: RegExp, options?: RegExpOptions): TRegExp +/** `[JavaScript]` Creates a RegExp type */ +export function RegExp(unresolved: RegExp | string, options?: RegExpOptions) { + const expr = IsString(unresolved) ? new globalThis.RegExp(unresolved) : unresolved + return CreateType({ [Kind]: 'RegExp', type: 'RegExp', source: expr.source, flags: expr.flags }, options) as never +} diff --git a/src/type/registry/format.ts b/src/type/registry/format.ts new file mode 100644 index 000000000..af818ae19 --- /dev/null +++ b/src/type/registry/format.ts @@ -0,0 +1,55 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export type FormatRegistryValidationFunction = (value: string) => boolean +/** A registry for user defined string formats */ +const map = new Map() +/** Returns the entries in this registry */ +export function Entries() { + return new Map(map) +} +/** Clears all user defined string formats */ +export function Clear() { + return map.clear() +} +/** Deletes a registered format */ +export function Delete(format: string) { + return map.delete(format) +} +/** Returns true if the user defined string format exists */ +export function Has(format: string) { + return map.has(format) +} +/** Sets a validation function for a user defined string format */ +export function Set(format: string, func: FormatRegistryValidationFunction) { + map.set(format, func) +} +/** Gets a validation function for a user defined string format */ +export function Get(format: string) { + return map.get(format) +} diff --git a/src/type/registry/index.ts b/src/type/registry/index.ts new file mode 100644 index 000000000..cdb2ac472 --- /dev/null +++ b/src/type/registry/index.ts @@ -0,0 +1,30 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * as FormatRegistry from './format' +export * as TypeRegistry from './type' diff --git a/src/type/registry/type.ts b/src/type/registry/type.ts new file mode 100644 index 000000000..955357a5b --- /dev/null +++ b/src/type/registry/type.ts @@ -0,0 +1,56 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export type TypeRegistryValidationFunction = (schema: TSchema, value: unknown) => boolean +/** A registry for user defined types */ + +const map = new Map>() +/** Returns the entries in this registry */ +export function Entries() { + return new Map(map) +} +/** Clears all user defined types */ +export function Clear() { + return map.clear() +} +/** Deletes a registered type */ +export function Delete(kind: string) { + return map.delete(kind) +} +/** Returns true if this registry contains this kind */ +export function Has(kind: string) { + return map.has(kind) +} +/** Sets a validation function for a user defined type */ +export function Set(kind: string, func: TypeRegistryValidationFunction) { + map.set(kind, func) +} +/** Gets a custom validation function for a user defined type */ +export function Get(kind: string) { + return map.get(kind) +} diff --git a/src/type/required/index.ts b/src/type/required/index.ts new file mode 100644 index 000000000..4570618f2 --- /dev/null +++ b/src/type/required/index.ts @@ -0,0 +1,30 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './required-from-mapped-result' +export * from './required' diff --git a/src/type/required/required-from-mapped-result.ts b/src/type/required/required-from-mapped-result.ts new file mode 100644 index 000000000..c5c2c30e7 --- /dev/null +++ b/src/type/required/required-from-mapped-result.ts @@ -0,0 +1,84 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { SchemaOptions } from '../schema/index' +import type { Ensure, Evaluate } from '../helpers/index' +import type { TProperties } from '../object/index' +import { MappedResult, type TMappedResult } from '../mapped/index' +import { Required, type TRequired } from './required' + +// ------------------------------------------------------------------ +// FromProperties +// ------------------------------------------------------------------ +// prettier-ignore +type TFromProperties< + P extends TProperties +> = ( + { [K2 in keyof P]: TRequired } +) +// prettier-ignore +function FromProperties< + P extends TProperties +>(P: P, options?: SchemaOptions): TFromProperties

{ + const Acc = {} as TProperties + for(const K2 of globalThis.Object.getOwnPropertyNames(P)) Acc[K2] = Required(P[K2], options) + return Acc as never +} +// ------------------------------------------------------------------ +// FromMappedResult +// ------------------------------------------------------------------ +// prettier-ignore +type TFromMappedResult< + R extends TMappedResult +> = ( + Evaluate> +) +// prettier-ignore +function FromMappedResult< + R extends TMappedResult +>(R: R, options?: SchemaOptions): TFromMappedResult { + return FromProperties(R.properties, options) as never +} +// ------------------------------------------------------------------ +// TRequiredFromMappedResult +// ------------------------------------------------------------------ +// prettier-ignore +export type TRequiredFromMappedResult< + R extends TMappedResult, + P extends TProperties = TFromMappedResult +> = ( + Ensure> +) +// prettier-ignore +export function RequiredFromMappedResult< + R extends TMappedResult, + P extends TProperties = TFromMappedResult +>(R: R, options?: SchemaOptions): TMappedResult

{ + const P = FromMappedResult(R, options) + return MappedResult(P) as never +} diff --git a/src/type/required/required.ts b/src/type/required/required.ts new file mode 100644 index 000000000..7ce1b8879 --- /dev/null +++ b/src/type/required/required.ts @@ -0,0 +1,189 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { CreateType } from '../create/type' +import type { TSchema, SchemaOptions } from '../schema/index' +import type { Evaluate, Ensure } from '../helpers/index' +import type { TMappedResult } from '../mapped/index' +import { type TReadonlyOptional } from '../readonly-optional/index' +import { type TComputed, Computed } from '../computed/index' +import { type TOptional } from '../optional/index' +import { type TReadonly } from '../readonly/index' +import { type TRecursive } from '../recursive/index' +import { type TObject, type TProperties, Object } from '../object/index' +import { type TIntersect, Intersect } from '../intersect/index' +import { type TUnion, Union } from '../union/index' +import { type TRef, Ref } from '../ref/index' +import { type TBigInt } from '../bigint/index' +import { type TBoolean } from '../boolean/index' +import { type TInteger } from '../integer/index' +import { type TLiteral } from '../literal/index' +import { type TNull } from '../null/index' +import { type TNumber } from '../number/index' +import { type TString } from '../string/index' +import { type TSymbol } from '../symbol/index' +import { type TUndefined } from '../undefined/index' + +import { OptionalKind, TransformKind } from '../symbols/index' +import { Discard } from '../discard/index' +import { RequiredFromMappedResult, type TRequiredFromMappedResult } from './required-from-mapped-result' + +// ------------------------------------------------------------------ +// TypeGuard +// ------------------------------------------------------------------ +import * as KindGuard from '../guard/kind' + +// ------------------------------------------------------------------ +// FromComputed +// ------------------------------------------------------------------ +// prettier-ignore +type TFromComputed = Ensure< + TComputed<'Required', [TComputed]> +> +// prettier-ignore +function FromComputed(target: Target, parameters: Parameters): TFromComputed { + return Computed('Required', [Computed(target, parameters)]) as never +} +// ------------------------------------------------------------------ +// FromRef +// ------------------------------------------------------------------ +// prettier-ignore +type TFromRef = Ensure< + TComputed<'Required', [TRef]> +> +// prettier-ignore +function FromRef($ref: Ref): TFromRef { + return Computed('Required', [Ref($ref)]) as never +} +// ------------------------------------------------------------------ +// FromProperties +// ------------------------------------------------------------------ +// prettier-ignore +type TFromProperties = Evaluate<{ + [K in keyof Properties]: + Properties[K] extends (TReadonlyOptional) ? TReadonly : + Properties[K] extends (TReadonly) ? TReadonly : + Properties[K] extends (TOptional) ? S : + Properties[K] +}> +// prettier-ignore +function FromProperties(properties: Properties) { + const requiredProperties = {} as TProperties + for(const K of globalThis.Object.getOwnPropertyNames(properties)) requiredProperties[K] = Discard(properties[K], [OptionalKind]) as TSchema + return requiredProperties as never +} +// ------------------------------------------------------------------ +// FromObject +// ------------------------------------------------------------------ +// prettier-ignore +type TFromObject = Ensure +)>> +// prettier-ignore +function FromObject(type: Type): TFromObject { + const options = Discard(type, [TransformKind, '$id', 'required', 'properties']) + const properties = FromProperties(type['properties']) + return Object(properties, options) as never +} +// ------------------------------------------------------------------ +// FromRest +// ------------------------------------------------------------------ +// prettier-ignore +type TFromRest = ( + Types extends [infer L extends TSchema, ...infer R extends TSchema[]] + ? TFromRest]> + : Result +) +// prettier-ignore +function FromRest(types: [...Types]) : TFromRest { + return types.map(type => RequiredResolve(type)) as never +} +// ------------------------------------------------------------------ +// RequiredResolve +// ------------------------------------------------------------------ +// prettier-ignore +function RequiredResolve(type: Type): TRequired { + return ( + // Mappable + KindGuard.IsComputed(type) ? FromComputed(type.target, type.parameters) : + KindGuard.IsRef(type) ? FromRef(type.$ref) : + KindGuard.IsIntersect(type) ? Intersect(FromRest(type.allOf)) : + KindGuard.IsUnion(type) ? Union(FromRest(type.anyOf)) : + KindGuard.IsObject(type) ? FromObject(type) : + // Intrinsic + KindGuard.IsBigInt(type) ? type : + KindGuard.IsBoolean(type) ? type : + KindGuard.IsInteger(type) ? type : + KindGuard.IsLiteral(type) ? type : + KindGuard.IsNull(type) ? type : + KindGuard.IsNumber(type) ? type : + KindGuard.IsString(type) ? type : + KindGuard.IsSymbol(type) ? type : + KindGuard.IsUndefined(type) ? type : + // Passthrough + Object({}) + ) as never +} +// ------------------------------------------------------------------ +// TRequired +// ------------------------------------------------------------------ +// prettier-ignore +export type TRequired = ( + // Mappable + Type extends TRecursive ? TRecursive> : + Type extends TComputed ? TFromComputed : + Type extends TRef ? TFromRef : + Type extends TIntersect ? TIntersect> : + Type extends TUnion ? TUnion> : + Type extends TObject ? TFromObject> : + // Intrinsic + Type extends TBigInt ? Type : + Type extends TBoolean ? Type : + Type extends TInteger ? Type : + Type extends TLiteral ? Type : + Type extends TNull ? Type : + Type extends TNumber ? Type : + Type extends TString ? Type : + Type extends TSymbol ? Type : + Type extends TUndefined ? Type : + // Passthrough + TObject<{}> +) +/** `[Json]` Constructs a type where all properties are required */ +export function Required(type: MappedResult, options?: SchemaOptions): TRequiredFromMappedResult +/** `[Json]` Constructs a type where all properties are required */ +export function Required(type: Type, options?: SchemaOptions): TRequired +/** `[Json]` Constructs a type where all properties are required */ +export function Required(type: Type, options?: SchemaOptions): never { + if (KindGuard.IsMappedResult(type)) { + return RequiredFromMappedResult(type, options) as never + } else { + // special: mapping types require overridable options + return CreateType({ ...RequiredResolve(type), ...options }) as never + } +} diff --git a/src/type/rest/index.ts b/src/type/rest/index.ts new file mode 100644 index 000000000..f4cfc39eb --- /dev/null +++ b/src/type/rest/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './rest' diff --git a/src/type/rest/rest.ts b/src/type/rest/rest.ts new file mode 100644 index 000000000..e467ed0c7 --- /dev/null +++ b/src/type/rest/rest.ts @@ -0,0 +1,64 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { TSchema } from '../schema/index' +import type { TIntersect } from '../intersect/index' +import type { TUnion } from '../union/index' +import type { TTuple } from '../tuple/index' + +// ------------------------------------------------------------------ +// TypeGuard +// ------------------------------------------------------------------ +import { IsIntersect, IsUnion, IsTuple } from '../guard/kind' +// ------------------------------------------------------------------ +// RestResolve +// ------------------------------------------------------------------ +// prettier-ignore +type TRestResolve = + T extends TIntersect ? S : + T extends TUnion ? S : + T extends TTuple ? S : + [] +// prettier-ignore +function RestResolve(T: T) { + return ( + IsIntersect(T) ? T.allOf : + IsUnion(T) ? T.anyOf : + IsTuple(T) ? T.items ?? [] : + [] + ) as never +} +// ------------------------------------------------------------------ +// TRest +// ------------------------------------------------------------------ +export type TRest = TRestResolve + +/** `[Json]` Extracts interior Rest elements from Tuple, Intersect and Union types */ +export function Rest(T: T): TRest { + return RestResolve(T) +} diff --git a/src/type/return-type/index.ts b/src/type/return-type/index.ts new file mode 100644 index 000000000..18d2290dd --- /dev/null +++ b/src/type/return-type/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './return-type' diff --git a/src/type/return-type/return-type.ts b/src/type/return-type/return-type.ts new file mode 100644 index 000000000..1b44968dd --- /dev/null +++ b/src/type/return-type/return-type.ts @@ -0,0 +1,45 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { CreateType } from '../create/type' +import { type TSchema, type SchemaOptions } from '../schema/index' +import { type TFunction } from '../function/index' +import { type TNever, Never } from '../never/index' +import * as KindGuard from '../guard/kind' + +// prettier-ignore +export type TReturnType + ? ReturnType + : TNever +> = Result + +/** `[JavaScript]` Extracts the ReturnType from the given Function type */ +export function ReturnType(schema: Type, options?: SchemaOptions): TReturnType { + return (KindGuard.IsFunction(schema) ? CreateType(schema.returns, options) : Never(options)) as never +} diff --git a/src/type/schema/anyschema.ts b/src/type/schema/anyschema.ts new file mode 100644 index 000000000..5a1d45675 --- /dev/null +++ b/src/type/schema/anyschema.ts @@ -0,0 +1,97 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +// ------------------------------------------------------------------ +// Type: Module +// ------------------------------------------------------------------ +import type { TAny } from '../any/index' +import type { TArray } from '../array/index' +import type { TAsyncIterator } from '../async-iterator/index' +import type { TBigInt } from '../bigint/index' +import type { TBoolean } from '../boolean/index' +import type { TConstructor } from '../constructor/index' +import type { TDate } from '../date/index' +import type { TEnum } from '../enum/index' +import type { TFunction } from '../function/index' +import type { TInteger } from '../integer/index' +import type { TIntersect } from '../intersect/index' +import type { TIterator } from '../iterator/index' +import type { TLiteral } from '../literal/index' +import type { TNot } from '../not/index' +import type { TNull } from '../null/index' +import type { TNumber } from '../number/index' +import type { TObject } from '../object/index' +import type { TPromise } from '../promise/index' +import type { TRecord } from '../record/index' +import type { TThis } from '../recursive/index' +import type { TRef } from '../ref/index' +import type { TRegExp } from '../regexp/index' +import type { TString } from '../string/index' +import type { TSymbol } from '../symbol/index' +import type { TTemplateLiteral } from '../template-literal/index' +import type { TTuple } from '../tuple/index' +import type { TUint8Array } from '../uint8array/index' +import type { TUndefined } from '../undefined/index' +import type { TUnion } from '../union/index' +import type { TUnknown } from '../unknown/index' +import type { TVoid } from '../void/index' +import type { TSchema } from './schema' + +export type TAnySchema = + | TSchema + | TAny + | TArray + | TAsyncIterator + | TBigInt + | TBoolean + | TConstructor + | TDate + | TEnum + | TFunction + | TInteger + | TIntersect + | TIterator + | TLiteral + | TNot + | TNull + | TNumber + | TObject + | TPromise + | TRecord + | TRef + | TRegExp + | TString + | TSymbol + | TTemplateLiteral + | TThis + | TTuple + | TUndefined + | TUnion + | TUint8Array + | TUnknown + | TVoid diff --git a/src/type/schema/index.ts b/src/type/schema/index.ts new file mode 100644 index 000000000..8279e8b4c --- /dev/null +++ b/src/type/schema/index.ts @@ -0,0 +1,30 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './anyschema' +export * from './schema' diff --git a/src/type/schema/schema.ts b/src/type/schema/schema.ts new file mode 100644 index 000000000..f4c967ed2 --- /dev/null +++ b/src/type/schema/schema.ts @@ -0,0 +1,58 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { Kind, Hint, ReadonlyKind, OptionalKind } from '../symbols/index' + +export interface SchemaOptions { + $schema?: string + /** Id for this schema */ + $id?: string + /** Title of this schema */ + title?: string + /** Description of this schema */ + description?: string + /** Default value for this schema */ + default?: any + /** Example values matching this schema */ + examples?: any + /** Optional annotation for readOnly */ + readOnly?: boolean + /** Optional annotation for writeOnly */ + writeOnly?: boolean + [prop: string]: any +} +export interface TKind { + [Kind]: string +} +export interface TSchema extends TKind, SchemaOptions { + [ReadonlyKind]?: string + [OptionalKind]?: string + [Hint]?: string + params: unknown[] + static: unknown +} diff --git a/src/type/sets/index.ts b/src/type/sets/index.ts new file mode 100644 index 000000000..df79d6aff --- /dev/null +++ b/src/type/sets/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './set' diff --git a/src/type/sets/set.ts b/src/type/sets/set.ts new file mode 100644 index 000000000..186d3f1c9 --- /dev/null +++ b/src/type/sets/set.ts @@ -0,0 +1,165 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +// ------------------------------------------------------------------ +// SetIncludes +// ------------------------------------------------------------------ +// prettier-ignore +export type TSetIncludes = ( + T extends [infer L extends PropertyKey, ...infer R extends PropertyKey[]] + ? S extends L + ? true + : TSetIncludes + : false +) +/** Returns true if element right is in the set of left */ +// prettier-ignore +export function SetIncludes(T: [...T], S: S): TSetIncludes { + return T.includes(S) as never +} +// ------------------------------------------------------------------ +// SetIsSubset +// ------------------------------------------------------------------ +// prettier-ignore +export type TSetIsSubset = ( + T extends [infer L extends PropertyKey, ...infer R extends PropertyKey[]] + ? TSetIncludes extends true + ? TSetIsSubset + : false + : true +) +/** Returns true if left is a subset of right */ +export function SetIsSubset(T: [...T], S: [...S]): TSetIsSubset { + return T.every((L) => SetIncludes(S, L)) as never +} +// ------------------------------------------------------------------ +// SetDistinct +// ------------------------------------------------------------------ +// prettier-ignore +export type TSetDistinct = + T extends [infer L extends PropertyKey, ...infer R extends PropertyKey[]] + ? TSetIncludes extends false + ? TSetDistinct + : TSetDistinct + : Acc +/** Returns a distinct set of elements */ +export function SetDistinct(T: [...T]): TSetDistinct { + return [...new Set(T)] as never +} +// ------------------------------------------------------------------ +// SetIntersect +// ------------------------------------------------------------------ +// prettier-ignore +export type TSetIntersect = ( + T extends [infer L extends PropertyKey, ...infer R extends PropertyKey[]] + ? TSetIncludes extends true + ? TSetIntersect + : TSetIntersect + : Acc +) +/** Returns the Intersect of the given sets */ +export function SetIntersect(T: [...T], S: [...S]): TSetIntersect { + return T.filter((L) => S.includes(L)) as never +} +// ------------------------------------------------------------------ +// SetUnion +// ------------------------------------------------------------------ +// prettier-ignore +export type TSetUnion = ( + [...T, ...S] +) +/** Returns the Union of the given sets */ +export function SetUnion(T: [...T], S: [...S]): TSetUnion { + return [...T, ...S] +} +// ------------------------------------------------------------------ +// SetComplement +// ------------------------------------------------------------------ +// prettier-ignore +export type TSetComplement = ( + T extends [infer L extends PropertyKey, ...infer R extends PropertyKey[]] + ? TSetIncludes extends true + ? TSetComplement + : TSetComplement + : Acc +) +/** Returns the Complement by omitting elements in T that are in S */ +// prettier-ignore +export function SetComplement(T: [...T], S: [...S]): TSetComplement { + return T.filter(L => !S.includes(L)) as never +} +// ------------------------------------------------------------------ +// SetIntersectMany +// ------------------------------------------------------------------ +// prettier-ignore +type TSetIntersectManyResolve = ( + T extends [infer L extends PropertyKey[], ...infer R extends PropertyKey[][]] + ? TSetIntersectManyResolve> + : Acc +) +// prettier-ignore +function SetIntersectManyResolve(T: [...T], Init: Acc): TSetIntersectManyResolve { + return T.reduce((Acc: PropertyKey[], L: PropertyKey[]) => { + return SetIntersect(Acc, L) + }, Init) as never +} +// prettier-ignore +export type TSetIntersectMany = ( + T extends [infer L extends PropertyKey[]] + ? L + // Use left to initialize the accumulator for resolve + : T extends [infer L extends PropertyKey[], ...infer R extends PropertyKey[][]] + ? TSetIntersectManyResolve + : [] +) +// prettier-ignore +export function SetIntersectMany(T: [...T]): TSetIntersectMany { + return ( + T.length === 1 + ? T[0] + // Use left to initialize the accumulator for resolve + : T.length > 1 + ? SetIntersectManyResolve(T.slice(1), T[0]) + : [] + ) as never +} +// ------------------------------------------------------------------ +// SetUnionMany +// ------------------------------------------------------------------ +// prettier-ignore +export type TSetUnionMany = ( + T extends [infer L extends PropertyKey[], ...infer R extends PropertyKey[][]] + ? TSetUnionMany> + : Acc +) +/** Returns the Union of multiple sets */ +export function SetUnionMany(T: [...T]): TSetUnionMany { + const Acc = [] as PropertyKey[] + for (const L of T) Acc.push(...L) + return Acc as never +} diff --git a/src/type/static/index.ts b/src/type/static/index.ts new file mode 100644 index 000000000..70c29b49d --- /dev/null +++ b/src/type/static/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './static' diff --git a/src/type/static/static.ts b/src/type/static/static.ts new file mode 100644 index 000000000..b85250600 --- /dev/null +++ b/src/type/static/static.ts @@ -0,0 +1,120 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { Evaluate } from '../helpers/index' +import type { TOptional } from '../optional/index' +import type { TReadonly } from '../readonly/index' +import type { TArray } from '../array/index' +import type { TAsyncIterator } from '../async-iterator/index' +import type { TConstructor } from '../constructor/index' +import type { TEnum } from '../enum/index' +import type { TFunction } from '../function/index' +import type { TIntersect } from '../intersect/index' +import type { TImport } from '../module/index' +import type { TIterator } from '../iterator/index' +import type { TNot } from '../not/index' +import type { TObject, TProperties } from '../object/index' +import type { TPromise } from '../promise/index' +import type { TRecursive } from '../recursive/index' +import type { TRecord } from '../record/index' +import type { TRef } from '../ref/index' +import type { TTuple } from '../tuple/index' +import type { TUnion } from '../union/index' +import type { TUnsafe } from '../unsafe/index' +import type { TSchema } from '../schema/index' +import type { TTransform } from '../transform/index' +import type { TNever } from '../never/index' + +// ------------------------------------------------------------------ +// Import +// ------------------------------------------------------------------ +// prettier-ignore +type TDecodeImport = ( + Key extends keyof ModuleProperties + ? TDecodeType extends infer Type extends TSchema + ? Type extends TRef + ? TDecodeImport + : Type + : TNever + : TNever +) +// ------------------------------------------------------------------ +// Properties +// ------------------------------------------------------------------ +// prettier-ignore +type TDecodeProperties = { + [Key in keyof Properties]: TDecodeType +} +// ------------------------------------------------------------------ +// Types +// ------------------------------------------------------------------ +// prettier-ignore +type TDecodeTypes = ( + Types extends [infer Left extends TSchema, ...infer Right extends TSchema[]] + ? TDecodeTypes]> + : Result +) +// ------------------------------------------------------------------ +// Types +// ------------------------------------------------------------------ +// prettier-ignore +export type TDecodeType = ( + Type extends TOptional ? TOptional> : + Type extends TReadonly ? TReadonly> : + Type extends TTransform ? TUnsafe : + Type extends TArray ? TArray> : + Type extends TAsyncIterator ? TAsyncIterator> : + Type extends TConstructor ? TConstructor, TDecodeType> : + Type extends TEnum ? TEnum : // intercept for union. interior non decodable + Type extends TFunction ? TFunction, TDecodeType> : + Type extends TIntersect ? TIntersect> : + Type extends TImport ? TDecodeImport : + Type extends TIterator ? TIterator> : + Type extends TNot ? TNot> : + Type extends TObject ? TObject>> : + Type extends TPromise ? TPromise> : + Type extends TRecord ? TRecord> : + Type extends TRecursive ? TRecursive> : + Type extends TRef ? TRef : + Type extends TTuple ? TTuple> : + Type extends TUnion ? TUnion> : + Type +) +// ------------------------------------------------------------------ +// Static +// ------------------------------------------------------------------ +export type StaticDecodeIsAny = boolean extends (Type extends TSchema ? true : false) ? true : false +/** Creates an decoded static type from a TypeBox type */ +// prettier-ignore +export type StaticDecode extends true ? unknown : Static, Params> +> = Result +/** Creates an encoded static type from a TypeBox type */ +export type StaticEncode> = Result +/** Creates a static type from a TypeBox type */ +export type Static = Result diff --git a/src/type/string/index.ts b/src/type/string/index.ts new file mode 100644 index 000000000..48ecacda4 --- /dev/null +++ b/src/type/string/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './string' diff --git a/src/type/string/string.ts b/src/type/string/string.ts new file mode 100644 index 000000000..52f2ae113 --- /dev/null +++ b/src/type/string/string.ts @@ -0,0 +1,87 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { CreateType } from '../create/type' +import { TSchema, SchemaOptions } from '../schema/index' +import { Kind } from '../symbols/index' + +// ------------------------------------------------------------------ +// TString +// ------------------------------------------------------------------ +export type StringFormatOption = + | 'date-time' + | 'time' + | 'date' + | 'email' + | 'idn-email' + | 'hostname' + | 'idn-hostname' + | 'ipv4' + | 'ipv6' + | 'uri' + | 'uri-reference' + | 'iri' + | 'uuid' + | 'iri-reference' + | 'uri-template' + | 'json-pointer' + | 'relative-json-pointer' + | 'regex' + | ({} & string) +// prettier-ignore +export type StringContentEncodingOption = + | '7bit' + | '8bit' + | 'binary' + | 'quoted-printable' + | 'base64' + | ({} & string) +export interface StringOptions extends SchemaOptions { + /** The maximum string length */ + maxLength?: number + /** The minimum string length */ + minLength?: number + /** A regular expression pattern this string should match */ + pattern?: string + /** A format this string should match */ + format?: StringFormatOption + /** The content encoding for this string */ + contentEncoding?: StringContentEncodingOption + /** The content media type for this string */ + contentMediaType?: string +} +export interface TString extends TSchema, StringOptions { + [Kind]: 'String' + static: string + type: 'string' +} + +/** `[Json]` Creates a String type */ +export function String(options?: StringOptions): TString { + return CreateType({ [Kind]: 'String', type: 'string' }, options) as never +} diff --git a/src/type/symbol/index.ts b/src/type/symbol/index.ts new file mode 100644 index 000000000..2e308a833 --- /dev/null +++ b/src/type/symbol/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './symbol' diff --git a/src/type/symbol/symbol.ts b/src/type/symbol/symbol.ts new file mode 100644 index 000000000..e60dae6e0 --- /dev/null +++ b/src/type/symbol/symbol.ts @@ -0,0 +1,42 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { CreateType } from '../create/type' +import { TSchema, SchemaOptions } from '../schema/index' +import { Kind } from '../symbols/index' + +export type TSymbolValue = string | number | undefined +export interface TSymbol extends TSchema, SchemaOptions { + [Kind]: 'Symbol' + static: symbol + type: 'symbol' +} +/** `[JavaScript]` Creates a Symbol type */ +export function Symbol(options?: SchemaOptions): TSymbol { + return CreateType({ [Kind]: 'Symbol', type: 'symbol' }, options) as never +} diff --git a/src/type/symbols/index.ts b/src/type/symbols/index.ts new file mode 100644 index 000000000..627854d7a --- /dev/null +++ b/src/type/symbols/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './symbols' diff --git a/src/type/symbols/symbols.ts b/src/type/symbols/symbols.ts new file mode 100644 index 000000000..8b47d1be6 --- /dev/null +++ b/src/type/symbols/symbols.ts @@ -0,0 +1,38 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +/** Symbol key applied to transform types */ +export const TransformKind = Symbol.for('TypeBox.Transform') +/** Symbol key applied to readonly types */ +export const ReadonlyKind = Symbol.for('TypeBox.Readonly') +/** Symbol key applied to optional types */ +export const OptionalKind = Symbol.for('TypeBox.Optional') +/** Symbol key applied to types */ +export const Hint = Symbol.for('TypeBox.Hint') +/** Symbol key applied to types */ +export const Kind = Symbol.for('TypeBox.Kind') diff --git a/src/type/template-literal/finite.ts b/src/type/template-literal/finite.ts new file mode 100644 index 000000000..fa5f5ea27 --- /dev/null +++ b/src/type/template-literal/finite.ts @@ -0,0 +1,119 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { TemplateLiteralParseExact } from './parse' +import { TypeBoxError } from '../error/index' +import type { TTemplateLiteral, TTemplateLiteralKind } from './index' +import type { TUnion } from '../union/index' +import type { TString } from '../string/index' +import type { TBoolean } from '../boolean/index' +import type { TNumber } from '../number/index' +import type { TInteger } from '../integer/index' +import type { TBigInt } from '../bigint/index' +import type { TLiteral } from '../literal/index' +import type { Expression } from './parse' + +// ------------------------------------------------------------------ +// TemplateLiteralFiniteError +// ------------------------------------------------------------------ +export class TemplateLiteralFiniteError extends TypeBoxError {} + +// ------------------------------------------------------------------ +// IsTemplateLiteralFiniteCheck +// ------------------------------------------------------------------ +// prettier-ignore +function IsNumberExpression(expression: Expression): boolean { + return ( + expression.type === 'or' && + expression.expr.length === 2 && + expression.expr[0].type === 'const' && + expression.expr[0].const === '0' && + expression.expr[1].type === 'const' && + expression.expr[1].const === '[1-9][0-9]*' + ) +} +// prettier-ignore +function IsBooleanExpression(expression: Expression): boolean { + return ( + expression.type === 'or' && + expression.expr.length === 2 && + expression.expr[0].type === 'const' && + expression.expr[0].const === 'true' && + expression.expr[1].type === 'const' && + expression.expr[1].const === 'false' + ) +} +// prettier-ignore +function IsStringExpression(expression: Expression) { + return expression.type === 'const' && expression.const === '.*' +} +// prettier-ignore +type TFromTemplateLiteralKind = + T extends TTemplateLiteral ? TFromTemplateLiteralKinds : + T extends TUnion ? TFromTemplateLiteralKinds : + T extends TString ? false : + T extends TNumber ? false : + T extends TInteger ? false : + T extends TBigInt ? false : + T extends TBoolean ? true : + T extends TLiteral ? true : + false +// prettier-ignore +type TFromTemplateLiteralKinds = + T extends [infer L extends TTemplateLiteralKind, ...infer R extends TTemplateLiteralKind[]] + ? TFromTemplateLiteralKind extends false + ? false + : TFromTemplateLiteralKinds : + true +// ------------------------------------------------------------------ +// IsTemplateLiteralExpressionFinite +// ------------------------------------------------------------------ +// prettier-ignore +export function IsTemplateLiteralExpressionFinite(expression: Expression): boolean { + return ( + IsNumberExpression(expression) || IsStringExpression(expression) ? false : + IsBooleanExpression(expression) ? true : + (expression.type === 'and') ? expression.expr.every((expr) => IsTemplateLiteralExpressionFinite(expr)) : + (expression.type === 'or') ? expression.expr.every((expr) => IsTemplateLiteralExpressionFinite(expr)) : + (expression.type === 'const') ? true : + (() => { throw new TemplateLiteralFiniteError(`Unknown expression type`) })() + ) +} +// ------------------------------------------------------------------ +// TIsTemplateLiteralFinite +// ------------------------------------------------------------------ +// prettier-ignore +export type TIsTemplateLiteralFinite = + T extends TTemplateLiteral + ? TFromTemplateLiteralKinds + : false +/** Returns true if this TemplateLiteral resolves to a finite set of values */ +export function IsTemplateLiteralFinite(schema: T) { + const expression = TemplateLiteralParseExact(schema.pattern) + return IsTemplateLiteralExpressionFinite(expression) +} diff --git a/src/type/template-literal/generate.ts b/src/type/template-literal/generate.ts new file mode 100644 index 000000000..56a0a0c6a --- /dev/null +++ b/src/type/template-literal/generate.ts @@ -0,0 +1,149 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { IsTemplateLiteralExpressionFinite, TIsTemplateLiteralFinite } from './finite' +import { TemplateLiteralParseExact } from './parse' +import { TypeBoxError } from '../error/index' + +import type { Assert } from '../helpers/index' +import type { TBoolean } from '../boolean/index' +import type { TTemplateLiteral, TTemplateLiteralKind } from './index' +import type { TLiteral, TLiteralValue } from '../literal/index' +import type { Expression, ExpressionAnd, ExpressionOr, ExpressionConst } from './parse' +import type { TUnion } from '../union/index' + +// ------------------------------------------------------------------ +// TemplateLiteralGenerateError +// ------------------------------------------------------------------ +export class TemplateLiteralGenerateError extends TypeBoxError {} +// ------------------------------------------------------------------ +// StringReducers +// ------------------------------------------------------------------ +// StringReduceUnary<"A", ["B", "C"]> -> ["AB", "AC"] +// prettier-ignore +type TStringReduceUnary = + R extends [infer A extends string, ...infer B extends string[]] + ? TStringReduceUnary + : Acc +// StringReduceBinary<['A', 'B'], ['C', 'D']> -> ["AC", "AD", "BC", "BD"] +// prettier-ignore +type TStringReduceBinary = + L extends [infer A extends string, ...infer B extends string[]] + ? TStringReduceBinary]> + : Acc +// StringReduceMany<[['A', 'B'], ['C', 'D'], ['E']]> -> [["ACE", "ADE", "BCE", "BDE"]] +// prettier-ignore +type TStringReduceMany = // consider optimizing + T extends [infer L extends string[], infer R extends string[], ...infer Rest extends string[][]] + ? TStringReduceMany<[TStringReduceBinary, ...Rest]> + : T +// Reduce<[['A', 'B'], ['C', 'D'], ['E']]> -> ["ACE", "ADE", "BCE", "BDE"] +// prettier-ignore +type TStringReduce> = + 0 extends keyof O + ? Assert + : [] +// ------------------------------------------------------------------ +// FromTemplateLiteralUnionKinds +// ------------------------------------------------------------------ +// prettier-ignore +type TFromTemplateLiteralUnionKinds = + T extends [infer L extends TLiteral, ...infer R extends TLiteral[]] + ? [`${L['const']}`, ...TFromTemplateLiteralUnionKinds] + : [] +// ------------------------------------------------------------------ +// FromTemplateLiteralKinds +// ------------------------------------------------------------------ +// prettier-ignore +type TFromTemplateLiteralKinds = + T extends [infer L extends TTemplateLiteralKind, ...infer R extends TTemplateLiteralKind[]] + ? ( + L extends TTemplateLiteral ? TFromTemplateLiteralKinds<[...S, ...R], Acc> : + L extends TLiteral ? TFromTemplateLiteralKinds : + L extends TUnion ? TFromTemplateLiteralKinds]> : + L extends TBoolean ? TFromTemplateLiteralKinds : + Acc + ) : Acc +// ------------------------------------------------------------------ +// TemplateLiteralExpressionGenerate +// ------------------------------------------------------------------ +// prettier-ignore +function* GenerateReduce(buffer: string[][]): IterableIterator { + if (buffer.length === 1) return yield* buffer[0] + for (const left of buffer[0]) { + for (const right of GenerateReduce(buffer.slice(1))) { + yield `${left}${right}` + } + } +} +// prettier-ignore +function* GenerateAnd(expression: ExpressionAnd): IterableIterator { + return yield* GenerateReduce(expression.expr.map((expr) => [...TemplateLiteralExpressionGenerate(expr)])) +} +// prettier-ignore +function* GenerateOr(expression: ExpressionOr): IterableIterator { + for (const expr of expression.expr) yield* TemplateLiteralExpressionGenerate(expr) +} +// prettier-ignore +function* GenerateConst(expression: ExpressionConst): IterableIterator { + return yield expression.const +} +export function* TemplateLiteralExpressionGenerate(expression: Expression): IterableIterator { + return expression.type === 'and' + ? yield* GenerateAnd(expression) + : expression.type === 'or' + ? yield* GenerateOr(expression) + : expression.type === 'const' + ? yield* GenerateConst(expression) + : (() => { + throw new TemplateLiteralGenerateError('Unknown expression') + })() +} +// ------------------------------------------------------------------ +// TTemplateLiteralGenerate +// ------------------------------------------------------------------ +// prettier-ignore +export type TTemplateLiteralGenerate> = + F extends true + ? ( + T extends TTemplateLiteral + ? TFromTemplateLiteralKinds extends infer R extends string[][] + ? TStringReduce + : [] + : [] + ) : [] +/** Generates a tuple of strings from the given TemplateLiteral. Returns an empty tuple if infinite. */ +export function TemplateLiteralGenerate(schema: T): TTemplateLiteralGenerate { + const expression = TemplateLiteralParseExact(schema.pattern) + // prettier-ignore + return ( + IsTemplateLiteralExpressionFinite(expression) + ? [...TemplateLiteralExpressionGenerate(expression)] + : [] + ) as never +} diff --git a/src/type/template-literal/index.ts b/src/type/template-literal/index.ts new file mode 100644 index 000000000..c06e4a639 --- /dev/null +++ b/src/type/template-literal/index.ts @@ -0,0 +1,35 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './finite' +export * from './generate' +export * from './syntax' +export * from './parse' +export * from './pattern' +export * from './union' +export * from './template-literal' diff --git a/src/type/template-literal/parse.ts b/src/type/template-literal/parse.ts new file mode 100644 index 000000000..1a6620f5d --- /dev/null +++ b/src/type/template-literal/parse.ts @@ -0,0 +1,186 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { TypeBoxError } from '../error/index' + +// ------------------------------------------------------------------ +// TemplateLiteralParserError +// ------------------------------------------------------------------ +export class TemplateLiteralParserError extends TypeBoxError {} +// ------------------------------------------------------------------ +// TemplateLiteralParse +// ------------------------------------------------------------------ +// prettier-ignore +export type Expression = ExpressionAnd | ExpressionOr | ExpressionConst +export type ExpressionConst = { type: 'const'; const: string } +export type ExpressionAnd = { type: 'and'; expr: Expression[] } +export type ExpressionOr = { type: 'or'; expr: Expression[] } +// ------------------------------------------------------------------- +// Unescape +// +// Unescape for these control characters specifically. Note that this +// function is only called on non union group content, and where we +// still want to allow the user to embed control characters in that +// content. For review. +// ------------------------------------------------------------------- +// prettier-ignore +function Unescape(pattern: string) { + return pattern + .replace(/\\\$/g, '$') + .replace(/\\\*/g, '*') + .replace(/\\\^/g, '^') + .replace(/\\\|/g, '|') + .replace(/\\\(/g, '(') + .replace(/\\\)/g, ')') +} +// ------------------------------------------------------------------- +// Control Characters +// ------------------------------------------------------------------- +function IsNonEscaped(pattern: string, index: number, char: string) { + return pattern[index] === char && pattern.charCodeAt(index - 1) !== 92 +} +function IsOpenParen(pattern: string, index: number) { + return IsNonEscaped(pattern, index, '(') +} +function IsCloseParen(pattern: string, index: number) { + return IsNonEscaped(pattern, index, ')') +} +function IsSeparator(pattern: string, index: number) { + return IsNonEscaped(pattern, index, '|') +} +// ------------------------------------------------------------------- +// Control Groups +// ------------------------------------------------------------------- +function IsGroup(pattern: string) { + if (!(IsOpenParen(pattern, 0) && IsCloseParen(pattern, pattern.length - 1))) return false + let count = 0 + for (let index = 0; index < pattern.length; index++) { + if (IsOpenParen(pattern, index)) count += 1 + if (IsCloseParen(pattern, index)) count -= 1 + if (count === 0 && index !== pattern.length - 1) return false + } + return true +} +// prettier-ignore +function InGroup(pattern: string) { + return pattern.slice(1, pattern.length - 1) +} +// prettier-ignore +function IsPrecedenceOr(pattern: string) { + let count = 0 + for (let index = 0; index < pattern.length; index++) { + if (IsOpenParen(pattern, index)) count += 1 + if (IsCloseParen(pattern, index)) count -= 1 + if (IsSeparator(pattern, index) && count === 0) return true + } + return false +} +// prettier-ignore +function IsPrecedenceAnd(pattern: string) { + for (let index = 0; index < pattern.length; index++) { + if (IsOpenParen(pattern, index)) return true + } + return false +} +// prettier-ignore +function Or(pattern: string): Expression { + let [count, start] = [0, 0] + const expressions: Expression[] = [] + for (let index = 0; index < pattern.length; index++) { + if (IsOpenParen(pattern, index)) count += 1 + if (IsCloseParen(pattern, index)) count -= 1 + if (IsSeparator(pattern, index) && count === 0) { + const range = pattern.slice(start, index) + if (range.length > 0) expressions.push(TemplateLiteralParse(range)) + start = index + 1 + } + } + const range = pattern.slice(start) + if (range.length > 0) expressions.push(TemplateLiteralParse(range)) + if (expressions.length === 0) return { type: 'const', const: '' } + if (expressions.length === 1) return expressions[0] + return { type: 'or', expr: expressions } +} +// prettier-ignore +function And(pattern: string): Expression { + function Group(value: string, index: number): [number, number] { + if (!IsOpenParen(value, index)) throw new TemplateLiteralParserError(`TemplateLiteralParser: Index must point to open parens`) + let count = 0 + for (let scan = index; scan < value.length; scan++) { + if (IsOpenParen(value, scan)) count += 1 + if (IsCloseParen(value, scan)) count -= 1 + if (count === 0) return [index, scan] + } + throw new TemplateLiteralParserError(`TemplateLiteralParser: Unclosed group parens in expression`) + } + function Range(pattern: string, index: number): [number, number] { + for (let scan = index; scan < pattern.length; scan++) { + if (IsOpenParen(pattern, scan)) return [index, scan] + } + return [index, pattern.length] + } + const expressions: Expression[] = [] + for (let index = 0; index < pattern.length; index++) { + if (IsOpenParen(pattern, index)) { + const [start, end] = Group(pattern, index) + const range = pattern.slice(start, end + 1) + expressions.push(TemplateLiteralParse(range)) + index = end + } else { + const [start, end] = Range(pattern, index) + const range = pattern.slice(start, end) + if (range.length > 0) expressions.push(TemplateLiteralParse(range)) + index = end - 1 + } + } + return ( + (expressions.length === 0) ? { type: 'const', const: '' } : + (expressions.length === 1) ? expressions[0] : + { type: 'and', expr: expressions } + ) +} +// ------------------------------------------------------------------ +// TemplateLiteralParse +// ------------------------------------------------------------------ +/** Parses a pattern and returns an expression tree */ +export function TemplateLiteralParse(pattern: string): Expression { + // prettier-ignore + return ( + IsGroup(pattern) ? TemplateLiteralParse(InGroup(pattern)) : + IsPrecedenceOr(pattern) ? Or(pattern) : + IsPrecedenceAnd(pattern) ? And(pattern) : + { type: 'const', const: Unescape(pattern) } + ) +} +// ------------------------------------------------------------------ +// TemplateLiteralParseExact +// ------------------------------------------------------------------ +/** Parses a pattern and strips forward and trailing ^ and $ */ +export function TemplateLiteralParseExact(pattern: string): Expression { + return TemplateLiteralParse(pattern.slice(1, pattern.length - 1)) +} diff --git a/src/type/template-literal/pattern.ts b/src/type/template-literal/pattern.ts new file mode 100644 index 000000000..4898583f0 --- /dev/null +++ b/src/type/template-literal/pattern.ts @@ -0,0 +1,67 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { TSchema } from '../schema/index' +import type { TTemplateLiteralKind } from './index' +import { PatternNumber, PatternString, PatternBoolean } from '../patterns/index' +import { Kind } from '../symbols/index' +import { TypeBoxError } from '../error/index' + +// ------------------------------------------------------------------ +// TypeGuard +// ------------------------------------------------------------------ +import { IsTemplateLiteral, IsUnion, IsNumber, IsInteger, IsBigInt, IsString, IsLiteral, IsBoolean } from '../guard/kind' + +// ------------------------------------------------------------------ +// TemplateLiteralPatternError +// ------------------------------------------------------------------ +export class TemplateLiteralPatternError extends TypeBoxError {} + +// ------------------------------------------------------------------ +// TemplateLiteralPattern +// ------------------------------------------------------------------ +function Escape(value: string) { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +} +// prettier-ignore +function Visit(schema: TSchema, acc: string): string { + return ( + IsTemplateLiteral(schema) ? schema.pattern.slice(1, schema.pattern.length - 1) : + IsUnion(schema) ? `(${schema.anyOf.map((schema) => Visit(schema, acc)).join('|')})` : + IsNumber(schema) ? `${acc}${PatternNumber}` : + IsInteger(schema) ? `${acc}${PatternNumber}` : + IsBigInt(schema) ? `${acc}${PatternNumber}` : + IsString(schema) ? `${acc}${PatternString}` : + IsLiteral(schema) ? `${acc}${Escape(schema.const.toString())}` : + IsBoolean(schema) ? `${acc}${PatternBoolean}` : + (() => { throw new TemplateLiteralPatternError(`Unexpected Kind '${schema[Kind]}'`) })() + ) +} +export function TemplateLiteralPattern(kinds: TTemplateLiteralKind[]): string { + return `^${kinds.map((schema) => Visit(schema, '')).join('')}\$` +} diff --git a/src/type/template-literal/syntax.ts b/src/type/template-literal/syntax.ts new file mode 100644 index 000000000..7f4804602 --- /dev/null +++ b/src/type/template-literal/syntax.ts @@ -0,0 +1,120 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { Assert, Trim } from '../helpers/index' +import type { TTemplateLiteral, TTemplateLiteralKind } from './index' +import { Literal, type TLiteral } from '../literal/index' +import { Boolean, type TBoolean } from '../boolean/index' +import { BigInt, type TBigInt } from '../bigint/index' +import { Number, type TNumber } from '../number/index' +import { String, type TString } from '../string/index' +import { UnionEvaluated, type TUnionEvaluated } from '../union/index' +import { Never } from '../never/index' + +// ------------------------------------------------------------------ +// SyntaxParsers +// ------------------------------------------------------------------ +// prettier-ignore +function* FromUnion(syntax: string): IterableIterator { + const trim = syntax.trim().replace(/"|'/g, '') + return ( + trim === 'boolean' ? yield Boolean() : + trim === 'number' ? yield Number() : + trim === 'bigint' ? yield BigInt() : + trim === 'string' ? yield String() : + yield (() => { + const literals = trim.split('|').map((literal) => Literal(literal.trim())) + return ( + literals.length === 0 ? Never() : + literals.length === 1 ? literals[0] : + UnionEvaluated(literals) + ) + })() + ) +} +// prettier-ignore +function* FromTerminal(syntax: string): IterableIterator { + if (syntax[1] !== '{') { + const L = Literal('$') + const R = FromSyntax(syntax.slice(1)) + return yield* [L, ...R] + } + for (let i = 2; i < syntax.length; i++) { + if (syntax[i] === '}') { + const L = FromUnion(syntax.slice(2, i)) + const R = FromSyntax(syntax.slice(i + 1)) + return yield* [...L, ...R] + } + } + yield Literal(syntax) +} +// prettier-ignore +function* FromSyntax(syntax: string): IterableIterator { + for (let i = 0; i < syntax.length; i++) { + if (syntax[i] === '$') { + const L = Literal(syntax.slice(0, i)) + const R = FromTerminal(syntax.slice(i)) + return yield* [L, ...R] + } + } + yield Literal(syntax) +} +// prettier-ignore +type FromUnionLiteral = + T extends `${infer L}|${infer R}` ? [TLiteral>, ...FromUnionLiteral] : + T extends `${infer L}` ? [TLiteral>] : + [] +type FromUnion = TUnionEvaluated> +// prettier-ignore +type FromTerminal = + T extends 'boolean' ? TBoolean : + T extends 'bigint' ? TBigInt : + T extends 'number' ? TNumber : + T extends 'string' ? TString : + FromUnion +// prettier-ignore +type FromString = + T extends `{${infer L}}${infer R}` ? [FromTerminal, ...FromString] : + // note: to correctly handle $ characters encoded in the sequence, we need to + // lookahead and test against opening and closing union groups. + T extends `${infer L}$\{${infer R1}\}${infer R2}` ? [TLiteral, ...FromString<`{${R1}}`>, ...FromString] : + T extends `${infer L}$\{${infer R1}\}` ? [TLiteral, ...FromString<`{${R1}}`>] : + T extends `${infer L}` ? [TLiteral] : + [] + +// ------------------------------------------------------------------ +// TTemplateLiteralSyntax +// ------------------------------------------------------------------ +// prettier-ignore +export type TTemplateLiteralSyntax = ( + TTemplateLiteral, TTemplateLiteralKind[]>> +) +/** Parses TemplateLiteralSyntax and returns a tuple of TemplateLiteralKinds */ +export function TemplateLiteralSyntax(syntax: string): TTemplateLiteralKind[] { + return [...FromSyntax(syntax)] +} diff --git a/src/type/template-literal/template-literal.ts b/src/type/template-literal/template-literal.ts new file mode 100644 index 000000000..794009f88 --- /dev/null +++ b/src/type/template-literal/template-literal.ts @@ -0,0 +1,103 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { CreateType } from '../create/type' +import type { TSchema, SchemaOptions } from '../schema/index' +import type { Assert } from '../helpers/index' +import type { TUnion } from '../union/index' +import type { TLiteral } from '../literal/index' +import type { TInteger } from '../integer/index' +import type { TNumber } from '../number/index' +import type { TBigInt } from '../bigint/index' +import type { TString } from '../string/index' +import type { TBoolean } from '../boolean/index' +import type { TNever } from '../never/index' +import type { Static } from '../static/index' + +import { TemplateLiteralSyntax, type TTemplateLiteralSyntax } from './syntax' +import { TemplateLiteralPattern } from './pattern' +import { EmptyString } from '../helpers/index' +import { IsString } from '../guard/value' +import { Kind } from '../symbols/index' + +// ------------------------------------------------------------------ +// TemplateLiteralStaticKind +// ------------------------------------------------------------------ +// prettier-ignore +type TemplateLiteralStaticKind = + T extends TUnion ? { [K in keyof U]: TemplateLiteralStatic, Acc> }[number] : + T extends TTemplateLiteral ? `${Static}` : + T extends TLiteral ? `${U}` : + T extends TString ? `${string}` : + T extends TNumber ? `${number}` : + T extends TBigInt ? `${bigint}` : + T extends TBoolean ? `${boolean}` : + never +// ------------------------------------------------------------------ +// TemplateLiteralStatic +// ------------------------------------------------------------------ +// prettier-ignore +type TemplateLiteralStatic = + T extends [infer L, ...infer R] ? `${TemplateLiteralStaticKind}${TemplateLiteralStatic, Acc>}` : + Acc +// ------------------------------------------------------------------ +// TTemplateLiteralKind +// ------------------------------------------------------------------ +// prettier-ignore +export type TTemplateLiteralKind = + | TTemplateLiteral + | TUnion + | TLiteral + | TInteger + | TNumber + | TBigInt + | TString + | TBoolean + | TNever +// ------------------------------------------------------------------ +// TTemplateLiteral +// ------------------------------------------------------------------ +// prettier-ignore +export interface TTemplateLiteral extends TSchema { + [Kind]: 'TemplateLiteral' + static: TemplateLiteralStatic + type: 'string' + pattern: string // todo: it may be possible to infer this pattern +} +/** `[Json]` Creates a TemplateLiteral type from template dsl string */ +export function TemplateLiteral(syntax: T, options?: SchemaOptions): TTemplateLiteralSyntax +/** `[Json]` Creates a TemplateLiteral type */ +export function TemplateLiteral(kinds: [...T], options?: SchemaOptions): TTemplateLiteral +/** `[Json]` Creates a TemplateLiteral type */ +// prettier-ignore +export function TemplateLiteral(unresolved: TTemplateLiteralKind[] | string, options?: SchemaOptions): any { + const pattern = IsString(unresolved) + ? TemplateLiteralPattern(TemplateLiteralSyntax(unresolved)) + : TemplateLiteralPattern(unresolved as TTemplateLiteralKind[]) + return CreateType({ [Kind]: 'TemplateLiteral', type: 'string', pattern }, options) +} diff --git a/src/type/template-literal/union.ts b/src/type/template-literal/union.ts new file mode 100644 index 000000000..44cae3781 --- /dev/null +++ b/src/type/template-literal/union.ts @@ -0,0 +1,56 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { Static } from '../static/index' +import type { TTemplateLiteral } from './template-literal' +import type { UnionToTuple } from '../helpers/index' +import { UnionEvaluated, type TUnionEvaluated } from '../union/index' +import { Literal, type TLiteral } from '../literal/index' +import { TemplateLiteralGenerate } from './generate' + +// ------------------------------------------------------------------ +// TemplateLiteralToUnion +// ------------------------------------------------------------------ +// prettier-ignore +export type TTemplateLiteralToUnionLiteralArray = ( + T extends [infer L extends string, ...infer R extends string[]] + ? TTemplateLiteralToUnionLiteralArray]> + : Acc +) +// prettier-ignore +export type TTemplateLiteralToUnion< + T extends TTemplateLiteral, + U extends string[] = UnionToTuple> +> = TUnionEvaluated> + +/** Returns a Union from the given TemplateLiteral */ +export function TemplateLiteralToUnion(schema: TTemplateLiteral): TTemplateLiteralToUnion { + const R = TemplateLiteralGenerate(schema) as string[] + const L = R.map((S) => Literal(S)) + return UnionEvaluated(L) +} diff --git a/src/type/transform/index.ts b/src/type/transform/index.ts new file mode 100644 index 000000000..da318ecf7 --- /dev/null +++ b/src/type/transform/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './transform' diff --git a/src/type/transform/transform.ts b/src/type/transform/transform.ts new file mode 100644 index 000000000..b89ac5f9b --- /dev/null +++ b/src/type/transform/transform.ts @@ -0,0 +1,86 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { TSchema } from '../schema/index' +import type { Static, StaticDecode } from '../static/index' +import { TransformKind } from '../symbols/index' + +// ------------------------------------------------------------------ +// TypeGuard +// ------------------------------------------------------------------ +import { IsTransform } from '../guard/kind' + +// ------------------------------------------------------------------ +// TransformBuilders +// ------------------------------------------------------------------ +export class TransformDecodeBuilder { + constructor(private readonly schema: T) {} + public Decode, U>>(decode: D): TransformEncodeBuilder { + return new TransformEncodeBuilder(this.schema, decode) + } +} +// prettier-ignore +export class TransformEncodeBuilder { + constructor(private readonly schema: T, private readonly decode: D) { } + private EncodeTransform, StaticDecode>>(encode: E, schema: TTransform) { + const Encode = (value: unknown) => schema[TransformKind as any].Encode(encode(value as any)) + const Decode = (value: unknown) => this.decode(schema[TransformKind as any].Decode(value)) + const Codec = { Encode: Encode, Decode: Decode } + return { ...schema, [TransformKind]: Codec } + } + private EncodeSchema, StaticDecode>>(encode: E, schema: TSchema) { + const Codec = { Decode: this.decode, Encode: encode } + return { ...schema, [TransformKind]: Codec } + } + public Encode, StaticDecode>>(encode: E): TTransform> { + return ( + IsTransform(this.schema) ? this.EncodeTransform(encode, this.schema): this.EncodeSchema(encode, this.schema) + ) as never + } +} +// ------------------------------------------------------------------ +// TransformStatic +// ------------------------------------------------------------------ +type TransformStatic = T extends TTransform ? S : Static +// ------------------------------------------------------------------ +// TTransform +// ------------------------------------------------------------------ +export type TransformFunction = (value: T) => U +export interface TransformOptions { + Decode: TransformFunction, O> + Encode: TransformFunction> +} +export interface TTransform extends TSchema { + static: TransformStatic + [TransformKind]: TransformOptions + [key: string]: any +} +/** `[Json]` Creates a Transform type */ +export function Transform(schema: I): TransformDecodeBuilder { + return new TransformDecodeBuilder(schema) +} diff --git a/src/type/tuple/index.ts b/src/type/tuple/index.ts new file mode 100644 index 000000000..ffe9c5a9e --- /dev/null +++ b/src/type/tuple/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './tuple' diff --git a/src/type/tuple/tuple.ts b/src/type/tuple/tuple.ts new file mode 100644 index 000000000..59caac85f --- /dev/null +++ b/src/type/tuple/tuple.ts @@ -0,0 +1,62 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { CreateType } from '../create/type' +import type { TSchema, SchemaOptions } from '../schema/index' +import type { Static } from '../static/index' +import { Kind } from '../symbols/index' + +// ------------------------------------------------------------------ +// TupleStatic +// ------------------------------------------------------------------ +// prettier-ignore +type TupleStatic = + T extends [infer L extends TSchema, ...infer R extends TSchema[]] + ? TupleStatic]> + : Acc +// ------------------------------------------------------------------ +// TTuple +// ------------------------------------------------------------------ +export interface TTuple extends TSchema { + [Kind]: 'Tuple' + static: TupleStatic + type: 'array' + items?: T + additionalItems?: false + minItems: number + maxItems: number +} +/** `[Json]` Creates a Tuple type */ +export function Tuple(types: [...Types], options?: SchemaOptions): TTuple { + // prettier-ignore + return CreateType( + types.length > 0 ? + { [Kind]: 'Tuple', type: 'array', items: types, additionalItems: false, minItems: types.length, maxItems: types.length } : + { [Kind]: 'Tuple', type: 'array', minItems: types.length, maxItems: types.length }, + options) as never +} diff --git a/src/type/type/index.ts b/src/type/type/index.ts new file mode 100644 index 000000000..3c82b269a --- /dev/null +++ b/src/type/type/index.ts @@ -0,0 +1,43 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +// ------------------------------------------------------------------ +// JsonTypeBuilder +// ------------------------------------------------------------------ +export { JsonTypeBuilder } from './json' + +// ------------------------------------------------------------------ +// JavaScriptTypeBuilder +// ------------------------------------------------------------------ +import * as TypeBuilder from './type' +import { JavaScriptTypeBuilder } from './javascript' + +/** JavaScript Type Builder with Static Resolution for TypeScript */ +const Type = TypeBuilder as InstanceType +export { JavaScriptTypeBuilder } +export { Type } diff --git a/src/type/type/javascript.ts b/src/type/type/javascript.ts new file mode 100644 index 000000000..daba64887 --- /dev/null +++ b/src/type/type/javascript.ts @@ -0,0 +1,133 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { JsonTypeBuilder } from './json' +import { Argument, type TArgument } from '../argument/index' +import { AsyncIterator, type TAsyncIterator } from '../async-iterator/index' +import { Awaited, type TAwaited } from '../awaited/index' +import { BigInt, type TBigInt, type BigIntOptions } from '../bigint/index' +import { Constructor, type TConstructor } from '../constructor/index' +import { ConstructorParameters, type TConstructorParameters } from '../constructor-parameters/index' +import { Date, type TDate, type DateOptions } from '../date/index' +import { Function as FunctionType, type TFunction } from '../function/index' +import { InstanceType, type TInstanceType } from '../instance-type/index' +import { Instantiate, type TInstantiate } from '../instantiate/index' +import { Iterator, type TIterator } from '../iterator/index' +import { Parameters, type TParameters } from '../parameters/index' +import { Promise, type TPromise } from '../promise/index' +import { RegExp, type TRegExp, RegExpOptions } from '../regexp/index' +import { ReturnType, type TReturnType } from '../return-type/index' +import { type TSchema, type SchemaOptions } from '../schema/index' +import { Symbol, type TSymbol } from '../symbol/index' +import { Uint8Array, type TUint8Array, type Uint8ArrayOptions } from '../uint8array/index' +import { Undefined, type TUndefined } from '../undefined/index' +import { Void, type TVoid } from '../void/index' + +/** JavaScript Type Builder with Static Resolution for TypeScript */ +export class JavaScriptTypeBuilder extends JsonTypeBuilder { + /** `[JavaScript]` Creates a Generic Argument Type */ + public Argument(index: Index): TArgument { + return Argument(index) + } + /** `[JavaScript]` Creates a AsyncIterator type */ + public AsyncIterator(items: Type, options?: SchemaOptions): TAsyncIterator { + return AsyncIterator(items, options) + } + /** `[JavaScript]` Constructs a type by recursively unwrapping Promise types */ + public Awaited(schema: Type, options?: SchemaOptions): TAwaited { + return Awaited(schema, options) + } + /** `[JavaScript]` Creates a BigInt type */ + public BigInt(options?: BigIntOptions): TBigInt { + return BigInt(options) + } + /** `[JavaScript]` Extracts the ConstructorParameters from the given Constructor type */ + public ConstructorParameters(schema: Type, options?: SchemaOptions): TConstructorParameters { + return ConstructorParameters(schema, options) + } + /** `[JavaScript]` Creates a Constructor type */ + public Constructor(parameters: [...Parameters], instanceType: InstanceType, options?: SchemaOptions): TConstructor { + return Constructor(parameters, instanceType, options) + } + /** `[JavaScript]` Creates a Date type */ + public Date(options: DateOptions = {}): TDate { + return Date(options) + } + /** `[JavaScript]` Creates a Function type */ + public Function(parameters: [...Parameters], returnType: ReturnType, options?: SchemaOptions): TFunction { + return FunctionType(parameters, returnType, options) + } + /** `[JavaScript]` Extracts the InstanceType from the given Constructor type */ + public InstanceType(schema: Type, options?: SchemaOptions): TInstanceType { + return InstanceType(schema, options) + } + /** `[JavaScript]` Instantiates a type with the given parameters */ + public Instantiate(schema: Type, parameters: [...Parameters]): TInstantiate { + return Instantiate(schema, parameters) + } + /** `[JavaScript]` Creates an Iterator type */ + public Iterator(items: Type, options?: SchemaOptions): TIterator { + return Iterator(items, options) + } + /** `[JavaScript]` Extracts the Parameters from the given Function type */ + public Parameters(schema: Type, options?: SchemaOptions): TParameters { + return Parameters(schema, options) + } + /** `[JavaScript]` Creates a Promise type */ + public Promise(item: Type, options?: SchemaOptions): TPromise { + return Promise(item, options) + } + /** `[JavaScript]` Creates a RegExp type */ + public RegExp(pattern: string, options?: RegExpOptions): TRegExp + /** `[JavaScript]` Creates a RegExp type */ + public RegExp(regex: RegExp, options?: RegExpOptions): TRegExp + /** `[JavaScript]` Creates a RegExp type */ + public RegExp(unresolved: string | RegExp, options?: RegExpOptions) { + return RegExp(unresolved as any, options) + } + /** `[JavaScript]` Extracts the ReturnType from the given Function type */ + public ReturnType(type: Type, options?: SchemaOptions): TReturnType { + return ReturnType(type, options) + } + /** `[JavaScript]` Creates a Symbol type */ + public Symbol(options?: SchemaOptions): TSymbol { + return Symbol(options) + } + /** `[JavaScript]` Creates a Undefined type */ + public Undefined(options?: SchemaOptions): TUndefined { + return Undefined(options) + } + /** `[JavaScript]` Creates a Uint8Array type */ + public Uint8Array(options?: Uint8ArrayOptions): TUint8Array { + return Uint8Array(options) + } + /** `[JavaScript]` Creates a Void type */ + public Void(options?: SchemaOptions): TVoid { + return Void(options) + } +} diff --git a/src/type/type/json.ts b/src/type/type/json.ts new file mode 100644 index 000000000..c29e046a0 --- /dev/null +++ b/src/type/type/json.ts @@ -0,0 +1,356 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { Any, type TAny } from '../any/index' +import { Array, type TArray, type ArrayOptions } from '../array/index' +import { Boolean, type TBoolean } from '../boolean/index' +import { Composite, type TComposite } from '../composite/index' +import { Const, type TConst } from '../const/index' +import { Enum, type TEnum, type TEnumKey, type TEnumValue } from '../enum/index' +import { Exclude, type TExclude, type TExcludeFromMappedResult, type TExcludeFromTemplateLiteral } from '../exclude/index' +import { Extends, type TExtends, type TExtendsFromMappedKey, type TExtendsFromMappedResult } from '../extends/index' +import { Extract, type TExtract, type TExtractFromMappedResult, type TExtractFromTemplateLiteral } from '../extract/index' +import { Index, TIndex, type TIndexPropertyKeys, type TIndexFromMappedKey, type TIndexFromMappedResult, type TIndexFromComputed } from '../indexed/index' +import { Integer, type IntegerOptions, type TInteger } from '../integer/index' +import { Intersect, type IntersectOptions } from '../intersect/index' +import { Capitalize, Uncapitalize, Lowercase, Uppercase, type TCapitalize, type TUncapitalize, type TLowercase, type TUppercase } from '../intrinsic/index' +import { KeyOf, type TKeyOf } from '../keyof/index' +import { Literal, type TLiteral, type TLiteralValue } from '../literal/index' +import { Mapped, type TMappedFunction, type TMapped, type TMappedResult } from '../mapped/index' +import { Never, type TNever } from '../never/index' +import { Not, type TNot } from '../not/index' +import { Null, type TNull } from '../null/index' +import { type TMappedKey } from '../mapped/index' +import { Module, TModule } from '../module/index' +import { Number, type TNumber, type NumberOptions } from '../number/index' +import { Object, type TObject, type TProperties, type ObjectOptions } from '../object/index' +import { Omit, type TOmit } from '../omit/index' +import { Optional, type TOptionalWithFlag, type TOptionalFromMappedResult } from '../optional/index' +import { Partial, type TPartial, type TPartialFromMappedResult } from '../partial/index' +import { Pick, type TPick } from '../pick/index' +import { Readonly, type TReadonlyWithFlag, type TReadonlyFromMappedResult } from '../readonly/index' +import { ReadonlyOptional, type TReadonlyOptional } from '../readonly-optional/index' +import { Record, type TRecordOrObject } from '../record/index' +import { Recursive, type TRecursive, type TThis } from '../recursive/index' +import { Ref, type TRef, type TRefUnsafe } from '../ref/index' +import { Required, type TRequired, type TRequiredFromMappedResult } from '../required/index' +import { Rest, type TRest } from '../rest/index' +import { type TSchema, type SchemaOptions } from '../schema/index' +import { String, type TString, type StringOptions } from '../string/index' +import { TemplateLiteral, type TTemplateLiteral, type TTemplateLiteralKind, type TTemplateLiteralSyntax } from '../template-literal/index' +import { Transform, TransformDecodeBuilder } from '../transform/index' +import { Tuple, type TTuple } from '../tuple/index' +import { Union } from '../union/index' +import { Unknown, type TUnknown } from '../unknown/index' +import { Unsafe, type TUnsafe, type UnsafeOptions } from '../unsafe/index' + +/** Json Type Builder with Static Resolution for TypeScript */ +export class JsonTypeBuilder { + // ------------------------------------------------------------------------ + // Modifiers + // ------------------------------------------------------------------------ + /** `[Json]` Creates a Readonly and Optional property */ + public ReadonlyOptional(type: Type): TReadonlyOptional { + return ReadonlyOptional(type) + } + /** `[Json]` Creates a Readonly property */ + public Readonly(type: Type, enable: Flag): TReadonlyFromMappedResult + /** `[Json]` Creates a Readonly property */ + public Readonly(type: Type, enable: Flag): TReadonlyWithFlag + /** `[Json]` Creates a Optional property */ + public Readonly(type: Type): TReadonlyFromMappedResult + /** `[Json]` Creates a Readonly property */ + public Readonly(type: Type): TReadonlyWithFlag + /** `[Json]` Creates a Readonly property */ + public Readonly(type: TSchema, enable?: boolean): any { + return Readonly(type, enable ?? true) + } + /** `[Json]` Creates a Optional property */ + public Optional(type: Type, enable: Flag): TOptionalFromMappedResult + /** `[Json]` Creates a Optional property */ + public Optional(type: Type, enable: Flag): TOptionalWithFlag + /** `[Json]` Creates a Optional property */ + public Optional(type: Type): TOptionalFromMappedResult + /** `[Json]` Creates a Optional property */ + public Optional(type: Type): TOptionalWithFlag + /** `[Json]` Creates a Optional property */ + public Optional(type: TSchema, enable?: boolean): any { + return Optional(type, enable ?? true) + } + // ------------------------------------------------------------------------ + // Types + // ------------------------------------------------------------------------ + /** `[Json]` Creates an Any type */ + public Any(options?: SchemaOptions): TAny { + return Any(options) + } + /** `[Json]` Creates an Array type */ + public Array(items: Type, options?: ArrayOptions): TArray { + return Array(items, options) + } + /** `[Json]` Creates a Boolean type */ + public Boolean(options?: SchemaOptions): TBoolean { + return Boolean(options) + } + /** `[Json]` Intrinsic function to Capitalize LiteralString types */ + public Capitalize(schema: T, options?: SchemaOptions): TCapitalize { + return Capitalize(schema, options) + } + /** `[Json]` Creates a Composite object type */ + public Composite(schemas: [...T], options?: ObjectOptions): TComposite { + return Composite(schemas, options) // (error) TS 5.4.0-dev - review TComposite implementation + } + /** `[JavaScript]` Creates a readonly const type from the given value. */ + public Const(value: T, options?: SchemaOptions): TConst { + return Const(value, options) + } + /** `[Json]` Creates a Enum type */ + public Enum>(item: T, options?: SchemaOptions): TEnum { + return Enum(item, options) + } + /** `[Json]` Constructs a type by excluding from unionType all union members that are assignable to excludedMembers */ + public Exclude(unionType: L, excludedMembers: R, options?: SchemaOptions): TExcludeFromMappedResult + /** `[Json]` Constructs a type by excluding from unionType all union members that are assignable to excludedMembers */ + public Exclude(unionType: L, excludedMembers: R, options?: SchemaOptions): TExcludeFromTemplateLiteral + /** `[Json]` Constructs a type by excluding from unionType all union members that are assignable to excludedMembers */ + public Exclude(unionType: L, excludedMembers: R, options?: SchemaOptions): TExclude + /** `[Json]` Constructs a type by excluding from unionType all union members that are assignable to excludedMembers */ + public Exclude(unionType: TSchema, excludedMembers: TSchema, options?: SchemaOptions): any { + return Exclude(unionType, excludedMembers, options) + } + /** `[Json]` Creates a Conditional type */ + public Extends(L: L, R: R, T: T, F: F, options?: SchemaOptions): TExtendsFromMappedResult + /** `[Json]` Creates a Conditional type */ + public Extends(L: L, R: R, T: T, F: F, options?: SchemaOptions): TExtendsFromMappedKey + /** `[Json]` Creates a Conditional type */ + public Extends(L: L, R: R, T: T, F: F, options?: SchemaOptions): TExtends + /** `[Json]` Creates a Conditional type */ + public Extends(L: L, R: R, T: T, F: F, options?: SchemaOptions) { + return Extends(L, R, T, F, options) + } + /** `[Json]` Constructs a type by extracting from type all union members that are assignable to union */ + public Extract(type: L, union: R, options?: SchemaOptions): TExtractFromMappedResult + /** `[Json]` Constructs a type by extracting from type all union members that are assignable to union */ + public Extract(type: L, union: R, options?: SchemaOptions): TExtractFromTemplateLiteral + /** `[Json]` Constructs a type by extracting from type all union members that are assignable to union */ + public Extract(type: L, union: R, options?: SchemaOptions): TExtract + /** `[Json]` Constructs a type by extracting from type all union members that are assignable to union */ + public Extract(type: TSchema, union: TSchema, options?: SchemaOptions): any { + return Extract(type, union, options) + } + /** `[Json]` Returns an Indexed property type for the given keys */ + public Index(type: Type, key: Key, options?: SchemaOptions): TIndexFromComputed + /** `[Json]` Returns an Indexed property type for the given keys */ + public Index(type: Type, key: Key, options?: SchemaOptions): TIndexFromComputed + /** `[Json]` Returns an Indexed property type for the given keys */ + public Index(type: Type, key: Key, options?: SchemaOptions): TIndexFromComputed + /** `[Json]` Returns an Indexed property type for the given keys */ + public Index(type: Type, mappedResult: MappedResult, options?: SchemaOptions): TIndexFromMappedResult + /** `[Json]` Returns an Indexed property type for the given keys */ + public Index(type: Type, mappedKey: MappedKey, options?: SchemaOptions): TIndexFromMappedKey + /** `[Json]` Returns an Indexed property type for the given keys */ + public Index>(T: Type, K: Key, options?: SchemaOptions): TIndex + /** `[Json]` Returns an Indexed property type for the given keys */ + public Index(type: Type, propertyKeys: readonly [...PropertyKeys], options?: SchemaOptions): TIndex + /** `[Json]` Returns an Indexed property type for the given keys */ + public Index(type: TSchema, key: any, options?: SchemaOptions): any { + return Index(type, key, options) + } + /** `[Json]` Creates an Integer type */ + public Integer(options?: IntegerOptions): TInteger { + return Integer(options) + } + /** `[Json]` Creates an Intersect type */ + public Intersect(types: [...Types], options?: IntersectOptions): Intersect { + return Intersect(types, options) + } + /** `[Json]` Creates a KeyOf type */ + public KeyOf(type: Type, options?: SchemaOptions): TKeyOf { + return KeyOf(type, options) as never + } + /** `[Json]` Creates a Literal type */ + public Literal(literalValue: LiteralValue, options?: SchemaOptions): TLiteral { + return Literal(literalValue, options) + } + /** `[Json]` Intrinsic function to Lowercase LiteralString types */ + public Lowercase(type: Type, options?: SchemaOptions): TLowercase { + return Lowercase(type, options) + } + /** `[Json]` Creates a Mapped object type */ + public Mapped, F extends TMappedFunction = TMappedFunction, R extends TMapped = TMapped>(key: K, map: F, options?: ObjectOptions): R + /** `[Json]` Creates a Mapped object type */ + public Mapped = TMappedFunction, R extends TMapped = TMapped>(key: [...K], map: F, options?: ObjectOptions): R + /** `[Json]` Creates a Mapped object type */ + public Mapped(key: any, map: TMappedFunction, options?: ObjectOptions): any { + return Mapped(key, map, options) + } + /** `[Json]` Creates a Type Definition Module. */ + public Module(properties: Properties): TModule { + return Module(properties) + } + /** `[Json]` Creates a Never type */ + public Never(options?: SchemaOptions): TNever { + return Never(options) + } + /** `[Json]` Creates a Not type */ + public Not(type: T, options?: SchemaOptions): TNot { + return Not(type, options) + } + /** `[Json]` Creates a Null type */ + public Null(options?: SchemaOptions): TNull { + return Null(options) + } + /** `[Json]` Creates a Number type */ + public Number(options?: NumberOptions): TNumber { + return Number(options) + } + /** `[Json]` Creates an Object type */ + public Object(properties: T, options?: ObjectOptions): TObject { + return Object(properties, options) + } + /** `[Json]` Constructs a type whose keys are picked from the given type */ + public Omit(type: Type, key: readonly [...Key], options?: SchemaOptions): TOmit + /** `[Json]` Constructs a type whose keys are picked from the given type */ + public Omit(type: Type, key: Key, options?: SchemaOptions): TOmit + /** `[Json]` Constructs a type whose keys are omitted from the given type */ + public Omit(schema: TSchema, selector: any, options?: SchemaOptions): any { + return Omit(schema, selector, options) + } + /** `[Json]` Constructs a type where all properties are optional */ + public Partial(type: MappedResult, options?: SchemaOptions): TPartialFromMappedResult + /** `[Json]` Constructs a type where all properties are optional */ + public Partial(type: Type, options?: SchemaOptions): TPartial + /** `[Json]` Constructs a type where all properties are optional */ + public Partial(type: TSchema, options?: SchemaOptions): any { + return Partial(type, options) + } + /** `[Json]` Constructs a type whose keys are picked from the given type */ + public Pick(type: Type, key: readonly [...Key], options?: SchemaOptions): TPick + /** `[Json]` Constructs a type whose keys are picked from the given type */ + public Pick(type: Type, key: Key, options?: SchemaOptions): TPick + /** `[Json]` Constructs a type whose keys are picked from the given type */ + public Pick(type: any, key: any, options?: SchemaOptions): any { + return Pick(type, key, options) + } + /** `[Json]` Creates a Record type */ + public Record(key: Key, value: Value, options?: ObjectOptions): TRecordOrObject { + return Record(key, value, options) + } + /** `[Json]` Creates a Recursive type */ + public Recursive(callback: (thisType: TThis) => T, options?: SchemaOptions): TRecursive { + return Recursive(callback, options) + } + + /** `[Json]` Creates a Ref type.*/ + public Ref($ref: Ref, options?: SchemaOptions): TRef + /** + * @deprecated `[Json]` Creates a Ref type. This signature was deprecated in 0.34.0 where Ref requires callers to pass + * a `string` value for the reference (and not a schema). + * + * To adhere to the 0.34.0 signature, Ref implementations should be updated to the following. + * + * ```typescript + * // pre-0.34.0 + * + * const T = Type.String({ $id: 'T' }) + * + * const R = Type.Ref(T) + * ``` + * should be changed to the following + * + * ```typescript + * // post-0.34.0 + * + * const T = Type.String({ $id: 'T' }) + * + * const R = Type.Unsafe>(Type.Ref('T')) + * ``` + * You can also create a generic function to replicate the pre-0.34.0 signature if required + * + * ```typescript + * const LegacyRef = (schema: T) => Type.Unsafe>(Type.Ref(schema.$id!)) + * ``` + */ + public Ref(type: Type, options?: SchemaOptions): TRefUnsafe + /** `[Json]` Creates a Ref type. The referenced type must contain a $id */ + public Ref(...args: any[]): unknown { + return Ref(args[0] as string, args[1]) + } + /** `[Json]` Constructs a type where all properties are required */ + public Required(type: MappedResult, options?: SchemaOptions): TRequiredFromMappedResult + /** `[Json]` Constructs a type where all properties are required */ + public Required(type: Type, options?: SchemaOptions): TRequired + /** `[Json]` Constructs a type where all properties are required */ + public Required(type: TSchema, options?: SchemaOptions): any { + return Required(type, options) + } + /** `[Json]` Extracts interior Rest elements from Tuple, Intersect and Union types */ + public Rest(type: Type): TRest { + return Rest(type) + } + /** `[Json]` Creates a String type */ + public String(options?: StringOptions): TString { + return String(options) + } + /** `[Json]` Creates a TemplateLiteral type from template dsl string */ + public TemplateLiteral(syntax: Syntax, options?: SchemaOptions): TTemplateLiteralSyntax + /** `[Json]` Creates a TemplateLiteral type */ + public TemplateLiteral(kinds: [...Kinds], options?: SchemaOptions): TTemplateLiteral + /** `[Json]` Creates a TemplateLiteral type */ + public TemplateLiteral(unresolved: TTemplateLiteralKind[] | string, options?: SchemaOptions) { + return TemplateLiteral(unresolved as any, options) + } + /** `[Json]` Creates a Transform type */ + public Transform(type: Type): TransformDecodeBuilder { + return Transform(type) + } + /** `[Json]` Creates a Tuple type */ + public Tuple(types: [...Types], options?: SchemaOptions): TTuple { + return Tuple(types, options) + } + /** `[Json]` Intrinsic function to Uncapitalize LiteralString types */ + public Uncapitalize(type: Type, options?: SchemaOptions): TUncapitalize { + return Uncapitalize(type, options) + } + /** `[Json]` Creates a Union type */ + public Union(types: [...Types], options?: SchemaOptions): Union { + return Union(types, options) + } + /** `[Json]` Creates an Unknown type */ + public Unknown(options?: SchemaOptions): TUnknown { + return Unknown(options) + } + /** `[Json]` Creates a Unsafe type that will infers as the generic argument T */ + public Unsafe(options?: UnsafeOptions): TUnsafe { + return Unsafe(options) + } + /** `[Json]` Intrinsic function to Uppercase LiteralString types */ + public Uppercase(schema: T, options?: SchemaOptions): TUppercase { + return Uppercase(schema, options) + } +} diff --git a/src/type/type/type.ts b/src/type/type/type.ts new file mode 100644 index 000000000..83fc32dc2 --- /dev/null +++ b/src/type/type/type.ts @@ -0,0 +1,90 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +// ------------------------------------------------------------------ +// Type: Module +// ------------------------------------------------------------------ +export { Any } from '../any/index' +export { Argument } from '../argument/index' +export { Array } from '../array/index' +export { AsyncIterator } from '../async-iterator/index' +export { Awaited } from '../awaited/index' +export { BigInt } from '../bigint/index' +export { Boolean } from '../boolean/index' +export { Composite } from '../composite/index' +export { Const } from '../const/index' +export { Constructor } from '../constructor/index' +export { ConstructorParameters } from '../constructor-parameters/index' +export { Date } from '../date/index' +export { Enum } from '../enum/index' +export { Exclude } from '../exclude/index' +export { Extends } from '../extends/index' +export { Extract } from '../extract/index' +export { Function } from '../function/index' +export { Index } from '../indexed/index' +export { InstanceType } from '../instance-type/index' +export { Instantiate } from '../instantiate/index' +export { Integer } from '../integer/index' +export { Intersect } from '../intersect/index' +export { Capitalize, Uncapitalize, Lowercase, Uppercase } from '../intrinsic/index' +export { Iterator } from '../iterator/index' +export { KeyOf } from '../keyof/index' +export { Literal } from '../literal/index' +export { Mapped } from '../mapped/index' +export { Module } from '../module/index' +export { Never } from '../never/index' +export { Not } from '../not/index' +export { Null } from '../null/index' +export { Number } from '../number/index' +export { Object } from '../object/index' +export { Omit } from '../omit/index' +export { Optional } from '../optional/index' +export { Parameters } from '../parameters/index' +export { Partial } from '../partial/index' +export { Pick } from '../pick/index' +export { Promise } from '../promise/index' +export { Readonly } from '../readonly/index' +export { ReadonlyOptional } from '../readonly-optional/index' +export { Record } from '../record/index' +export { Recursive } from '../recursive/index' +export { Ref } from '../ref/index' +export { RegExp } from '../regexp/index' +export { Required } from '../required/index' +export { Rest } from '../rest/index' +export { ReturnType } from '../return-type/index' +export { String } from '../string/index' +export { Symbol } from '../symbol/index' +export { TemplateLiteral } from '../template-literal/index' +export { Transform } from '../transform/index' +export { Tuple } from '../tuple/index' +export { Uint8Array } from '../uint8array/index' +export { Undefined } from '../undefined/index' +export { Union } from '../union/index' +export { Unknown } from '../unknown/index' +export { Unsafe } from '../unsafe/index' +export { Void } from '../void/index' diff --git a/src/type/uint8array/index.ts b/src/type/uint8array/index.ts new file mode 100644 index 000000000..b81391707 --- /dev/null +++ b/src/type/uint8array/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './uint8array' diff --git a/src/type/uint8array/uint8array.ts b/src/type/uint8array/uint8array.ts new file mode 100644 index 000000000..8a8f31845 --- /dev/null +++ b/src/type/uint8array/uint8array.ts @@ -0,0 +1,45 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { CreateType } from '../create/type' +import type { TSchema, SchemaOptions } from '../schema/index' +import { Kind } from '../symbols/index' + +export interface Uint8ArrayOptions extends SchemaOptions { + maxByteLength?: number + minByteLength?: number +} +export interface TUint8Array extends TSchema, Uint8ArrayOptions { + [Kind]: 'Uint8Array' + static: Uint8Array + type: 'uint8array' +} +/** `[JavaScript]` Creates a Uint8Array type */ +export function Uint8Array(options?: Uint8ArrayOptions): TUint8Array { + return CreateType({ [Kind]: 'Uint8Array', type: 'Uint8Array' }, options) as never +} diff --git a/src/type/undefined/index.ts b/src/type/undefined/index.ts new file mode 100644 index 000000000..209e971cd --- /dev/null +++ b/src/type/undefined/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './undefined' diff --git a/src/type/undefined/undefined.ts b/src/type/undefined/undefined.ts new file mode 100644 index 000000000..66919ebac --- /dev/null +++ b/src/type/undefined/undefined.ts @@ -0,0 +1,41 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { CreateType } from '../create/type' +import type { TSchema, SchemaOptions } from '../schema/index' +import { Kind } from '../symbols/index' + +export interface TUndefined extends TSchema { + [Kind]: 'Undefined' + static: undefined + type: 'undefined' +} +/** `[JavaScript]` Creates a Undefined type */ +export function Undefined(options?: SchemaOptions): TUndefined { + return CreateType({ [Kind]: 'Undefined', type: 'undefined' }, options) as never +} diff --git a/src/type/union/index.ts b/src/type/union/index.ts new file mode 100644 index 000000000..bc694b24d --- /dev/null +++ b/src/type/union/index.ts @@ -0,0 +1,31 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './union-evaluated' +export * from './union-type' +export * from './union' diff --git a/src/type/union/union-create.ts b/src/type/union/union-create.ts new file mode 100644 index 000000000..0989fd967 --- /dev/null +++ b/src/type/union/union-create.ts @@ -0,0 +1,36 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { TSchema, SchemaOptions } from '../schema/index' +import { CreateType } from '../create/type' +import { TUnion } from './union-type' +import { Kind } from '../symbols/index' + +export function UnionCreate(T: [...T], options?: SchemaOptions): TUnion { + return CreateType({ [Kind]: 'Union', anyOf: T }, options) as never +} diff --git a/src/type/union/union-evaluated.ts b/src/type/union/union-evaluated.ts new file mode 100644 index 000000000..b3441294b --- /dev/null +++ b/src/type/union/union-evaluated.ts @@ -0,0 +1,126 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { CreateType } from '../create/type' +import type { SchemaOptions, TSchema } from '../schema/index' +import { OptionalKind } from '../symbols/index' +import { Discard } from '../discard/index' +import { Never, type TNever } from '../never/index' +import { Optional, type TOptional } from '../optional/index' +import type { TReadonly } from '../readonly/index' +import type { TUnion } from './union-type' +import { UnionCreate } from './union-create' + +// ------------------------------------------------------------------ +// TypeGuard +// ------------------------------------------------------------------ +import { IsOptional } from '../guard/kind' +// ------------------------------------------------------------------ +// IsUnionOptional +// ------------------------------------------------------------------ +// prettier-ignore +type TIsUnionOptional = ( + Types extends [infer Left extends TSchema, ...infer Right extends TSchema[]] ? + Left extends TOptional + ? true + : TIsUnionOptional + : false +) +// prettier-ignore +function IsUnionOptional(types: Types): TIsUnionOptional { + return types.some(type => IsOptional(type)) as never +} +// ------------------------------------------------------------------ +// RemoveOptionalFromRest +// ------------------------------------------------------------------ +// prettier-ignore +type TRemoveOptionalFromRest = ( + Types extends [infer Left extends TSchema, ...infer Right extends TSchema[]] + ? Left extends TOptional + ? TRemoveOptionalFromRest]> + : TRemoveOptionalFromRest + : Result +) +// prettier-ignore +function RemoveOptionalFromRest(types: Types): TRemoveOptionalFromRest { + return types.map(left => IsOptional(left) ? RemoveOptionalFromType(left) : left) as never +} +// ------------------------------------------------------------------ +// RemoveOptionalFromType +// ------------------------------------------------------------------ +// prettier-ignore +type TRemoveOptionalFromType = ( + Type extends TReadonly ? TReadonly> : + Type extends TOptional ? TRemoveOptionalFromType : + Type +) +// prettier-ignore +function RemoveOptionalFromType(T: Type): TRemoveOptionalFromType { + return ( + Discard(T, [OptionalKind]) + ) as never +} +// ------------------------------------------------------------------ +// ResolveUnion +// ------------------------------------------------------------------ +// prettier-ignore +type TResolveUnion, + IsOptional extends boolean = TIsUnionOptional +> = ( + IsOptional extends true + ? TOptional> + : TUnion +) +// prettier-ignore +function ResolveUnion(types: Types, options?: SchemaOptions): TResolveUnion { + const isOptional = IsUnionOptional(types) + return ( + isOptional + ? Optional(UnionCreate(RemoveOptionalFromRest(types) as TSchema[], options)) + : UnionCreate(RemoveOptionalFromRest(types) as TSchema[], options) + ) as never +} +// ------------------------------------------------------------------ +// Union +// ------------------------------------------------------------------ +// prettier-ignore +export type TUnionEvaluated = ( + Types extends [TSchema] ? Types[0] : + Types extends [] ? TNever : + TResolveUnion +) +/** `[Json]` Creates an evaluated Union type */ +export function UnionEvaluated>(T: [...Types], options?: SchemaOptions): Result { + // prettier-ignore + return ( + T.length === 1 ? CreateType(T[0], options) : + T.length === 0 ? Never(options) : + ResolveUnion(T, options) + ) as never +} diff --git a/src/type/union/union-type.ts b/src/type/union/union-type.ts new file mode 100644 index 000000000..245791fda --- /dev/null +++ b/src/type/union/union-type.ts @@ -0,0 +1,48 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { TSchema } from '../schema/index' +import type { Static } from '../static/index' +import { Kind } from '../symbols/index' + +// ------------------------------------------------------------------ +// UnionStatic +// ------------------------------------------------------------------ +// prettier-ignore +type UnionStatic = { + [K in keyof T]: T[K] extends TSchema ? Static : never +}[number] + +// ------------------------------------------------------------------ +// TUnion +// ------------------------------------------------------------------ +export interface TUnion extends TSchema { + [Kind]: 'Union' + static: UnionStatic + anyOf: T +} diff --git a/src/type/union/union.ts b/src/type/union/union.ts new file mode 100644 index 000000000..e91ba8c5a --- /dev/null +++ b/src/type/union/union.ts @@ -0,0 +1,49 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { TSchema, SchemaOptions } from '../schema/index' +import { type TNever, Never } from '../never/index' +import type { TUnion } from './union-type' +import { CreateType } from '../create/type' +import { UnionCreate } from './union-create' + +// prettier-ignore +export type Union = ( + T extends [] ? TNever : + T extends [TSchema] ? T[0] : + TUnion +) +/** `[Json]` Creates a Union type */ +export function Union(types: [...Types], options?: SchemaOptions): Union { + // prettier-ignore + return ( + types.length === 0 ? Never(options) : + types.length === 1 ? CreateType(types[0], options) : + UnionCreate(types, options) + ) as Union +} diff --git a/src/type/unknown/index.ts b/src/type/unknown/index.ts new file mode 100644 index 000000000..6070bc428 --- /dev/null +++ b/src/type/unknown/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './unknown' diff --git a/src/type/unknown/unknown.ts b/src/type/unknown/unknown.ts new file mode 100644 index 000000000..4c2406996 --- /dev/null +++ b/src/type/unknown/unknown.ts @@ -0,0 +1,40 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { CreateType } from '../create/type' +import type { TSchema, SchemaOptions } from '../schema/index' +import { Kind } from '../symbols/index' + +export interface TUnknown extends TSchema { + [Kind]: 'Unknown' + static: unknown +} +/** `[Json]` Creates an Unknown type */ +export function Unknown(options?: SchemaOptions): TUnknown { + return CreateType({ [Kind]: 'Unknown' }, options) as never +} diff --git a/src/type/unsafe/index.ts b/src/type/unsafe/index.ts new file mode 100644 index 000000000..6f3db72a5 --- /dev/null +++ b/src/type/unsafe/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './unsafe' diff --git a/src/type/unsafe/unsafe.ts b/src/type/unsafe/unsafe.ts new file mode 100644 index 000000000..133a1927d --- /dev/null +++ b/src/type/unsafe/unsafe.ts @@ -0,0 +1,43 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { CreateType } from '../create/type' +import type { TSchema, SchemaOptions } from '../schema/index' +import { Kind } from '../symbols/index' + +export interface UnsafeOptions extends SchemaOptions { + [Kind]?: string +} +export interface TUnsafe extends TSchema { + [Kind]: string + static: T +} +/** `[Json]` Creates a Unsafe type that will infers as the generic argument T */ +export function Unsafe(options: UnsafeOptions = {}): TUnsafe { + return CreateType({ [Kind]: options[Kind] ?? 'Unsafe' }, options) as never +} diff --git a/src/type/void/index.ts b/src/type/void/index.ts new file mode 100644 index 000000000..ee941af09 --- /dev/null +++ b/src/type/void/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './void' diff --git a/src/type/void/void.ts b/src/type/void/void.ts new file mode 100644 index 000000000..dec74a5e5 --- /dev/null +++ b/src/type/void/void.ts @@ -0,0 +1,41 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/type + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { CreateType } from '../create/type' +import type { TSchema, SchemaOptions } from '../schema/index' +import { Kind } from '../symbols/index' + +export interface TVoid extends TSchema { + [Kind]: 'Void' + static: void + type: 'void' +} +/** `[JavaScript]` Creates a Void type */ +export function Void(options?: SchemaOptions): TVoid { + return CreateType({ [Kind]: 'Void', type: 'void' }, options) as never +} diff --git a/src/typebox.ts b/src/typebox.ts deleted file mode 100644 index db18c4cc4..000000000 --- a/src/typebox.ts +++ /dev/null @@ -1,517 +0,0 @@ -/*-------------------------------------------------------------------------- - -TypeBox: JSON Schema Type Builder with Static Type Resolution for TypeScript - -The MIT License (MIT) - -Copyright (c) 2021 Haydn Paterson (sinclair) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - ----------------------------------------------------------------------------*/ - -// -------------------------------------------------------------------------- -// Modifiers -// -------------------------------------------------------------------------- - -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 } -export type TOptional = T & { modifier: typeof OptionalModifier } -export type TReadonly = T & { modifier: typeof ReadonlyModifier } - -// -------------------------------------------------------------------------- -// Schema Standard -// -------------------------------------------------------------------------- - -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 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 const RefKind = Symbol('RefKind') - -export interface CustomOptions { - $id?: string - title?: string - description?: string - default?: any - examples?: any - [prop: string]: any -} - -export type StringFormatOption = - | 'date-time' | 'time' | 'date' | 'email' | 'idn-email' | 'hostname' - | 'idn-hostname' | 'ipv4' | 'ipv6' | 'uri' | 'uri-reference' | 'iri' - | 'uuid' | 'iri-reference' | 'uri-template' | 'json-pointer' | 'relative-json-pointer' - | 'regex' - -export declare type StringOptions = { - minLength?: number - maxLength?: number - pattern?: string - format?: TFormat - contentEncoding?: '7bit' | '8bit' | 'binary' | 'quoted-printable' | 'base64' - contentMediaType?: string -} & CustomOptions - -export type ArrayOptions = { - uniqueItems?: boolean - minItems?: number - maxItems?: number -} & CustomOptions - -export type NumberOptions = { - exclusiveMaximum?: number - exclusiveMinimum?: number - maximum?: number - minimum?: number - multipleOf?: number -} & CustomOptions - -export type IntersectOptions = { - unevaluatedProperties?: boolean -} & CustomOptions - -export type ObjectOptions = { - additionalProperties?: boolean -} & CustomOptions - -// -------------------------------------------------------------------------- -// Namespacing -// -------------------------------------------------------------------------- - -export type TDefinitions = { [key: string]: TSchema } -export type TNamespace = { kind: typeof BoxKind, $defs: T } & CustomOptions - -// -------------------------------------------------------------------------- -// TSchema -// -------------------------------------------------------------------------- - -export interface TSchema { $static: unknown } - -// -------------------------------------------------------------------------- -// Standard Schema Types -// -------------------------------------------------------------------------- - -export type TEnumType = Record -export type TKey = string | number | symbol -export type TValue = string | number | boolean -export type TRecordKey = TString | TNumber | TKeyOf | TUnion -export type TEnumKey = { type: 'number' | 'string', const: T } - -export interface TProperties { [key: string]: TSchema } -export interface TRecord extends TSchema, ObjectOptions { $static: StaticRecord, kind: typeof RecordKind, type: 'object', patternProperties: { [pattern: string]: T } } -export interface TTuple extends TSchema, CustomOptions { $static: StaticTuple, kind: typeof TupleKind, type: 'array', items?: T, additionalItems?: false, minItems: number, maxItems: number } -export interface TObject extends TSchema, ObjectOptions { $static: StaticObject, kind: typeof ObjectKind, type: 'object', properties: T, required?: string[] } -export interface TUnion extends TSchema, CustomOptions { $static: StaticUnion, kind: typeof UnionKind, anyOf: T } -export interface TIntersect extends TSchema, IntersectOptions { $static: StaticIntersect, kind: typeof IntersectKind, type: 'object', allOf: T } -export interface TKeyOf extends TSchema, CustomOptions { $static: StaticKeyOf, kind: typeof KeyOfKind, type: 'string', enum: T } -export interface TArray extends TSchema, ArrayOptions { $static: StaticArray, kind: typeof ArrayKind, type: 'array', items: T } -export interface TLiteral extends TSchema, CustomOptions { $static: StaticLiteral, kind: typeof LiteralKind, const: T } -export interface TEnum extends TSchema, CustomOptions { $static: StaticEnum, kind: typeof EnumKind, anyOf: T } -export interface TString extends TSchema, StringOptions { $static: string, kind: typeof StringKind, type: 'string' } -export interface TNumber extends TSchema, NumberOptions { $static: number, kind: typeof NumberKind, type: 'number' } -export interface TInteger extends TSchema, NumberOptions { $static: number, kind: typeof IntegerKind, type: 'integer' } -export interface TBoolean extends TSchema, CustomOptions { $static: boolean, kind: typeof BooleanKind, type: 'boolean' } -export interface TNull extends TSchema, CustomOptions { $static: null, kind: typeof NullKind, type: 'null' } -export interface TUnknown extends TSchema, CustomOptions { $static: unknown, kind: typeof UnknownKind } -export interface TAny extends TSchema, CustomOptions { $static: any, kind: typeof AnyKind } -export interface TRef extends TSchema, CustomOptions { $static: Static, kind: typeof RefKind, $ref: string } - -// -------------------------------------------------------------------------- -// Extended Schema Types -// -------------------------------------------------------------------------- - -export const ConstructorKind = Symbol('ConstructorKind') -export const FunctionKind = Symbol('FunctionKind') -export const PromiseKind = Symbol('PromiseKind') -export const UndefinedKind = Symbol('UndefinedKind') -export const VoidKind = Symbol('VoidKind') -export interface TConstructor extends TSchema, CustomOptions { $static: StaticConstructor, kind: typeof ConstructorKind, type: 'constructor', arguments: TSchema[], returns: TSchema } -export interface TFunction extends TSchema, CustomOptions { $static: StaticFunction, kind: typeof FunctionKind, type: 'function', arguments: TSchema[], returns: TSchema } -export interface TPromise extends TSchema, CustomOptions { $static: StaticPromise, kind: typeof PromiseKind, type: 'promise', item: TSchema } -export interface TUndefined extends TSchema, CustomOptions { $static: undefined, kind: typeof UndefinedKind, type: 'undefined' } -export interface TVoid extends TSchema, CustomOptions { $static: void, kind: typeof VoidKind, type: 'void' } - -// -------------------------------------------------------------------------- -// Utility Types -// -------------------------------------------------------------------------- -export type Pickable = TObject | TRef> -export type PickablePropertyKeys = T extends TObject ? keyof U : T extends TRef> ? keyof U : never -export type PickableProperties = T extends TObject ? U : T extends TRef> ? U : never - -export type UnionToIntersect = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never -export type StaticReadonlyOptionalPropertyKeys = { [K in keyof T]: T[K] extends TReadonlyOptional ? K : never }[keyof T] -export type StaticReadonlyPropertyKeys = { [K in keyof T]: T[K] extends TReadonly ? K : never }[keyof T] -export type StaticOptionalPropertyKeys = { [K in keyof T]: T[K] extends TOptional ? K : never }[keyof T] -export type StaticRequiredPropertyKeys = keyof Omit | StaticReadonlyPropertyKeys | StaticOptionalPropertyKeys> -export type StaticIntersectEvaluate = {[K in keyof T]: T[K] extends TSchema ? Static : never } -export type StaticIntersectReduce = T extends [infer A, ...infer B] ? StaticIntersectReduce : I -export type StaticRequired = { - [K in keyof T]: - T[K] extends TReadonlyOptional ? TReadonly : - T[K] extends TReadonly ? TReadonly : - T[K] extends TOptional ? U : - T[K] -} -export type StaticPartial = { - [K in keyof T]: - T[K] extends TReadonlyOptional ? TReadonlyOptional : - T[K] extends TReadonly ? TReadonlyOptional : - T[K] extends TOptional ? TOptional : - TOptional -} - -// ------------------------------------------------------------------------ -// Static Schema -// ------------------------------------------------------------------------ - -export type StaticProperties = - { readonly [K in StaticReadonlyOptionalPropertyKeys]?: Static } & - { readonly [K in StaticReadonlyPropertyKeys]: Static } & - { [K in StaticOptionalPropertyKeys]?: Static } & - { [K in StaticRequiredPropertyKeys]: Static } -export type StaticRecord = - K extends TString ? Record> : - K extends TNumber ? Record> : - K extends TKeyOf ? Record> : - K extends TUnion ? Record> : - never -export type StaticEnum = T extends TEnumKey[] ? U : never -export type StaticKeyOf = T extends Array ? K : never -export type StaticIntersect = StaticIntersectReduce> -export type StaticUnion = { [K in keyof T]: T[K] extends TSchema ? Static : never }[number] -export type StaticTuple = { [K in keyof T]: T[K] extends TSchema ? Static : never } -export type StaticObject = StaticProperties extends infer I ? {[K in keyof I]: I[K]}: never -export type StaticArray = Array> -export type StaticLiteral = T -export type StaticParameters = {[K in keyof T]: T[K] extends TSchema ? Static: never } -export type StaticConstructor = new (...args: [...StaticParameters]) => Static -export type StaticFunction = (...args: [...StaticParameters]) => Static -export type StaticPromise = Promise> -export type Static = T['$static'] - -// -------------------------------------------------------------------------- -// Utility -// -------------------------------------------------------------------------- - -function isObject(object: any) { - return typeof object === 'object' && object !== null && !Array.isArray(object) -} - -function isArray(object: any) { - return typeof object === 'object' && object !== null && Array.isArray(object) -} - -function clone(object: any): any { - if(isObject(object)) return Object.keys(object).reduce((acc, key) => ({...acc, [key]: clone(object[key]) }), {}) - if(isArray(object)) return object.map((item: any) => clone(item)) - return object -} - -// -------------------------------------------------------------------------- -// TypeBuilder -// -------------------------------------------------------------------------- - -export class TypeBuilder { - private readonly schemas = new Map() - - /** `Standard` Modifies an object property to be both readonly and optional */ - public ReadonlyOptional(item: T): TReadonlyOptional { - return { ...item, modifier: ReadonlyOptionalModifier } - } - - /** `Standard` Modifies an object property to be readonly */ - public Readonly(item: T): TReadonly { - return { ...item, modifier: ReadonlyModifier } - } - - /** `Standard` Modifies an object property to be optional */ - public Optional(item: T): TOptional { - return { ...item, modifier: OptionalModifier } - } - - /** `Standard` Creates a type type */ - public Tuple(items: [...T], options: CustomOptions = {}): TTuple { - const additionalItems = false - const minItems = items.length - const maxItems = items.length - const schema = ((items.length > 0) - ? { ...options, kind: TupleKind, type: 'array', items, additionalItems, minItems, maxItems } - : { ...options, kind: TupleKind, type: 'array', minItems, maxItems }) as any - return this.Store(schema) - } - - /** `Standard` Creates an object type with the given properties */ - public Object(properties: T, options: ObjectOptions = {}): TObject { - const property_names = Object.keys(properties) - const optional = property_names.filter(name => { - const candidate = properties[name] as TModifier - return (candidate.modifier && - (candidate.modifier === OptionalModifier || - candidate.modifier === ReadonlyOptionalModifier)) - }) - const required_names = property_names.filter(name => !optional.includes(name)) - const required = (required_names.length > 0) ? required_names : undefined - return this.Store(((required) - ? { ...options, kind: ObjectKind, type: 'object', properties, required } - : { ...options, kind: ObjectKind, type: 'object', properties })) - } - - /** `Standard` Creates an intersect type. */ - public Intersect(items: [...T], options: IntersectOptions = {}): TIntersect { - return this.Store({ ...options, kind: IntersectKind, type: 'object', allOf: items }) - } - - /** `Standard` Creates a union type */ - public Union(items: [...T], options: CustomOptions = {}): TUnion { - return this.Store({ ...options, kind: UnionKind, anyOf: items }) - } - - /** `Standard` Creates an array type */ - public Array(items: T, options: ArrayOptions = {}): TArray { - return this.Store({ ...options, kind: ArrayKind, type: 'array', items }) - } - - /** `Standard` Creates an enum type from a TypeScript enum */ - public Enum(item: T, options: CustomOptions = {}): TEnum[]> { - const values = Object.keys(item).filter(key => isNaN(key as any)).map(key => item[key]) as T[keyof T][] - const anyOf = values.map(value => typeof value === 'string' ? { type: 'string' as const, const: value } : { type: 'number' as const, const: value }) - return this.Store({ ...options, kind: EnumKind, anyOf }) - } - - /** `Standard` Creates a literal type. Supports string, number and boolean values only */ - public Literal(value: T, options: CustomOptions = {}): TLiteral { - return this.Store({ ...options, kind: LiteralKind, const: value, type: typeof value as 'string' | 'number' | 'boolean' }) - } - - /** `Standard` Creates a string type */ - public String(options: StringOptions = {}): TString { - return this.Store({ ...options, kind: StringKind, type: 'string' }) - } - - /** `Standard` Creates a string type from a regular expression */ - public RegEx(regex: RegExp, options: CustomOptions = {}): TString { - return this.String({ ...options, pattern: regex.source }) - } - - /** `Standard` Creates a number type */ - public Number(options: NumberOptions = {}): TNumber { - return this.Store({ ...options, kind: NumberKind, type: 'number' }) - } - - /** `Standard` Creates an integer type */ - public Integer(options: NumberOptions = {}): TInteger { - return this.Store({ ...options, kind: IntegerKind, type: 'integer' }) - } - - /** `Standard` Creates a boolean type */ - public Boolean(options: CustomOptions = {}): TBoolean { - return this.Store({ ...options, kind: BooleanKind, type: 'boolean' }) - } - - /** `Standard` Creates a null type */ - public Null(options: CustomOptions = {}): TNull { - return this.Store({ ...options, kind: NullKind, type: 'null' }) - } - - /** `Standard` Creates an unknown type */ - public Unknown(options: CustomOptions = {}): TUnknown { - return this.Store({ ...options, kind: UnknownKind }) - } - - /** `Standard` Creates an any type */ - public Any(options: CustomOptions = {}): TAny { - return this.Store({ ...options, kind: AnyKind }) - } - - /** `Standard` Creates a keyof type from the given object */ - public KeyOf>(object: T, options: CustomOptions = {}): TKeyOf<(keyof T['properties'])[]> { - const keys = Object.keys(object.properties) - return this.Store({...options, kind: KeyOfKind, type: 'string', enum: keys }) - } - - /** `Standard` Creates a record type */ - public Record(key: K, value: T, options: ObjectOptions = {}): TRecord { - const pattern = (() => { - switch(key.kind) { - case UnionKind: return `^${key.anyOf.map((literal: any) => literal.const as TValue).join('|')}$` - case KeyOfKind: return `^${key.enum.join('|')}$` - case NumberKind: return '^(0|[1-9][0-9]*)$' - case StringKind: return key.pattern ? key.pattern : '^.*$' - default: throw Error('Invalid Record Key') - } - })() - return this.Store({ ...options, kind: RecordKind, type: 'object', patternProperties: { [pattern]: value } }) - } - - /** `Standard` Makes all properties in the given object type required */ - public Required | TRef>>(object: T, options: ObjectOptions = {}): TObject> { - const source = this.Resolve(object) - const schema = { ...clone(source), ...options } - schema.required = Object.keys(schema.properties) - for(const key of Object.keys(schema.properties)) { - const property = schema.properties[key] - switch(property.modifier) { - case ReadonlyOptionalModifier: property.modifier = ReadonlyModifier; break; - case ReadonlyModifier: property.modifier = ReadonlyModifier; break; - case OptionalModifier: delete property.modifier; break; - default: delete property.modifier; break; - } - } - return this.Store(schema) - } - - /** `Standard` Makes all properties in the given object type optional */ - public Partial | TRef>>(object: T, options: ObjectOptions = {}): TObject> { - const source = this.Resolve(object) - const schema = { ...clone(source), ...options } - delete schema.required - for(const key of Object.keys(schema.properties)) { - const property = schema.properties[key] - switch(property.modifier) { - case ReadonlyOptionalModifier: property.modifier = ReadonlyOptionalModifier; break; - case ReadonlyModifier: property.modifier = ReadonlyOptionalModifier; break; - case OptionalModifier: property.modifier = OptionalModifier; break; - default: property.modifier = OptionalModifier; break; - } - } - return this.Store(schema) - } - - /** `Standard` Picks property keys from the given object type */ - public Pick | TRef>, K extends PickablePropertyKeys[]>(object: T, keys: [...K], options: ObjectOptions = {}): TObject, K[number]>> { - const source = this.Resolve(object) - const schema = { ...clone(source), ...options } - schema.required = schema.required ? schema.required.filter((key: K) => keys.includes(key as any)) : undefined - for(const key of Object.keys(schema.properties)) { - if(!keys.includes(key as any)) delete schema.properties[key] - } - return this.Store(schema) - } - - /** `Standard` Omits property keys from the given object type */ - public Omit | TRef>, K extends PickablePropertyKeys[]>(object: T, keys: [...K], options: ObjectOptions = {}):TObject, K[number]>> { - const source = this.Resolve(object) - const schema = { ...clone(source), ...options } - schema.required = schema.required ? schema.required.filter((key: string) => !keys.includes(key as any)) : undefined - for(const key of Object.keys(schema.properties)) { - if(keys.includes(key as any)) delete schema.properties[key] - } - return this.Store(schema) - } - - /** `Standard` Omits the `kind` and `modifier` properties from the underlying schema */ - public Strict(schema: T, options: CustomOptions = {}): T { - return JSON.parse(JSON.stringify({ ...options, ...schema })) as T - } - - /** `Extended` Creates a constructor type */ - public Constructor(args: [...T], returns: U, options: CustomOptions = {}): TConstructor { - return this.Store({ ...options, kind: ConstructorKind, type: 'constructor', arguments: args, returns }) - } - - /** `Extended` Creates a function type */ - public Function(args: [...T], returns: U, options: CustomOptions = {}): TFunction { - return this.Store({ ...options, kind: FunctionKind, type: 'function', arguments: args, returns }) - } - - /** `Extended` Creates a promise type */ - public Promise(item: T, options: CustomOptions = {}): TPromise { - return this.Store({ ...options, type: 'promise', kind: PromiseKind, item }) - } - - /** `Extended` Creates a undefined type */ - public Undefined(options: CustomOptions = {}): TUndefined { - return this.Store({ ...options, type: 'undefined', kind: UndefinedKind }) - } - - /** `Extended` Creates a void type */ - public Void(options: CustomOptions = {}): TVoid { - return this.Store({ ...options, type: 'void', kind: VoidKind }) - } - - /** `Standard` Creates a namespace for a set of related types */ - public Namespace($defs: T, options: CustomOptions = {}): TNamespace { - return this.Store({ ...options, kind: BoxKind, $defs }) - } - - /** `Standard` References a type within a namespace. The referenced namespace must specify an `$id` */ - public Ref, K extends keyof T['$defs']>(box: T, key: K): TRef - - /** `Standard` References type. The referenced type must specify an `$id` */ - public Ref(schema: T): TRef - - public Ref(...args: any[]): any { - if(args.length === 2) { - const namespace = args[0] as TNamespace - const targetKey = args[1] as string - if(namespace.$id === undefined) throw new Error(`Referenced namespace has no $id`) - if(!this.schemas.has(namespace.$id)) throw new Error(`Unable to locate namespace with $id '${namespace.$id}'`) - return this.Store({ kind: RefKind, $ref: `${namespace.$id}#/$defs/${targetKey}` }) - } else if(args.length === 1) { - const target = args[0] as any - if(target.$id === undefined) throw new Error(`Referenced schema has no $id`) - if(!this.schemas.has(target.$id)) throw new Error(`Unable to locate schema with $id '${target.$id}'`) - return this.Store({ kind: RefKind, $ref: target.$id }) - } else { - throw new Error('Type.Ref: Invalid arguments') - } - } - - /** `Experimental` Creates a recursive type */ - public Rec(callback: (self: TAny) => T, options: CustomOptions = {}): T { - const $id = options.$id || '' - const self = callback({ $ref: `${$id}#/$defs/self` } as any) - return this.Store({ ...options, $ref: `${$id}#/$defs/self`, $defs: { self } }) - } - - /** Stores this schema if it contains an $id. This function is used for later referencing. */ - private Store(schema: any): any { - if(!schema.$id) return schema - this.schemas.set(schema.$id, schema) - return schema - } - - /** Resolves a schema by $id. May resolve recursively if the target is a TRef. */ - private Resolve(schema: any): any { - if(schema.kind !== RefKind) return schema - if(!this.schemas.has(schema.$ref)) throw Error(`Unable to locate schema with $id '${schema.$ref}'`) - return this.Resolve(this.schemas.get(schema.$ref)!) - } -} - - -export const Type = new TypeBuilder() diff --git a/src/value/assert/assert.ts b/src/value/assert/assert.ts new file mode 100644 index 000000000..57c0eebba --- /dev/null +++ b/src/value/assert/assert.ts @@ -0,0 +1,73 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/value + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { Errors, ValueErrorIterator, ValueError } from '../../errors/index' +import { TypeBoxError } from '../../type/error/error' +import { TSchema } from '../../type/schema/index' +import { Static } from '../../type/static/index' +import { Check } from '../check/check' + +// ------------------------------------------------------------------ +// AssertError +// ------------------------------------------------------------------ +export class AssertError extends TypeBoxError { + readonly #iterator: ValueErrorIterator + error: ValueError | undefined + constructor(iterator: ValueErrorIterator) { + const error = iterator.First() + super(error === undefined ? 'Invalid Value' : error.message) + this.#iterator = iterator + this.error = error + } + /** Returns an iterator for each error in this value. */ + public Errors(): ValueErrorIterator { + return new ValueErrorIterator(this.#Iterator()) + } + *#Iterator(): IterableIterator { + if (this.error) yield this.error + yield* this.#iterator + } +} +// ------------------------------------------------------------------ +// AssertValue +// ------------------------------------------------------------------ +function AssertValue(schema: TSchema, references: TSchema[], value: unknown): unknown { + if (Check(schema, references, value)) return + throw new AssertError(Errors(schema, references, value)) +} +// ------------------------------------------------------------------ +// Assert +// ------------------------------------------------------------------ +/** Asserts a value matches the given type or throws an `AssertError` if invalid */ +export function Assert(schema: T, references: TSchema[], value: unknown): asserts value is Static +/** Asserts a value matches the given type or throws an `AssertError` if invalid */ +export function Assert(schema: T, value: unknown): asserts value is Static +/** Asserts a value matches the given type or throws an `AssertError` if invalid */ +export function Assert(...args: any[]): unknown { + return args.length === 3 ? AssertValue(args[0], args[1], args[2]) : AssertValue(args[0], [], args[1]) +} diff --git a/src/value/assert/index.ts b/src/value/assert/index.ts new file mode 100644 index 000000000..c7adc3b1e --- /dev/null +++ b/src/value/assert/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/value + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './assert' diff --git a/src/value/cast/cast.ts b/src/value/cast/cast.ts new file mode 100644 index 000000000..28fe3028f --- /dev/null +++ b/src/value/cast/cast.ts @@ -0,0 +1,266 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/value + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { IsObject, IsArray, IsString, IsNumber, IsNull } from '../guard/index' +import { TypeBoxError } from '../../type/error/index' +import { Kind } from '../../type/symbols/index' +import { Create } from '../create/index' +import { Check } from '../check/index' +import { Clone } from '../clone/index' +import { Deref, Pushref } from '../deref/index' + +import type { TSchema } from '../../type/schema/index' +import type { Static } from '../../type/static/index' +import type { TArray } from '../../type/array/index' +import type { TConstructor } from '../../type/constructor/index' +import type { TImport } from '../../type/module/index' +import type { TIntersect } from '../../type/intersect/index' +import type { TObject } from '../../type/object/index' +import type { TRecord } from '../../type/record/index' +import type { TRef } from '../../type/ref/index' +import type { TThis } from '../../type/recursive/index' +import type { TTuple } from '../../type/tuple/index' +import type { TUnion } from '../../type/union/index' +import type { TNever } from '../../type/never/index' + +// ------------------------------------------------------------------ +// Errors +// ------------------------------------------------------------------ +export class ValueCastError extends TypeBoxError { + constructor(public readonly schema: TSchema, message: string) { + super(message) + } +} +// ------------------------------------------------------------------ +// The following logic assigns a score to a schema based on how well +// it matches a given value. For object types, the score is calculated +// by evaluating each property of the value against the schema's +// properties. To avoid bias towards objects with many properties, +// each property contributes equally to the total score. Properties +// that exactly match literal values receive the highest possible +// score, as literals are often used as discriminators in union types. +// ------------------------------------------------------------------ +function ScoreUnion(schema: TSchema, references: TSchema[], value: any): number { + if (schema[Kind] === 'Object' && typeof value === 'object' && !IsNull(value)) { + const object = schema as TObject + const keys = Object.getOwnPropertyNames(value) + const entries = Object.entries(object.properties) + return entries.reduce((acc, [key, schema]) => { + const literal = schema[Kind] === 'Literal' && schema.const === value[key] ? 100 : 0 + const checks = Check(schema, references, value[key]) ? 10 : 0 + const exists = keys.includes(key) ? 1 : 0 + return acc + (literal + checks + exists) + }, 0) + } else if (schema[Kind] === 'Union') { + const schemas = schema.anyOf.map((schema: TSchema) => Deref(schema, references)) + const scores = schemas.map((schema: TSchema) => ScoreUnion(schema, references, value)) + return Math.max(...scores) + } else { + return Check(schema, references, value) ? 1 : 0 + } +} +function SelectUnion(union: TUnion, references: TSchema[], value: any): TSchema { + const schemas = union.anyOf.map((schema) => Deref(schema, references)) + let [select, best] = [schemas[0], 0] + for (const schema of schemas) { + const score = ScoreUnion(schema, references, value) + if (score > best) { + select = schema + best = score + } + } + return select +} +function CastUnion(union: TUnion, references: TSchema[], value: any) { + if ('default' in union) { + return typeof value === 'function' ? union.default : Clone(union.default) + } else { + const schema = SelectUnion(union, references, value) + return Cast(schema, references, value) + } +} + +// ------------------------------------------------------------------ +// Default +// ------------------------------------------------------------------ +function DefaultClone(schema: TSchema, references: TSchema[], value: any): any { + return Check(schema, references, value) ? Clone(value) : Create(schema, references) +} +function Default(schema: TSchema, references: TSchema[], value: any): any { + return Check(schema, references, value) ? value : Create(schema, references) +} +// ------------------------------------------------------------------ +// Cast +// ------------------------------------------------------------------ +function FromArray(schema: TArray, references: TSchema[], value: any): any { + if (Check(schema, references, value)) return Clone(value) + const created = IsArray(value) ? Clone(value) : Create(schema, references) + const minimum = IsNumber(schema.minItems) && created.length < schema.minItems ? [...created, ...Array.from({ length: schema.minItems - created.length }, () => null)] : created + const maximum = IsNumber(schema.maxItems) && minimum.length > schema.maxItems ? minimum.slice(0, schema.maxItems) : minimum + const casted = maximum.map((value: unknown) => Visit(schema.items, references, value)) + if (schema.uniqueItems !== true) return casted + const unique = [...new Set(casted)] + if (!Check(schema, references, unique)) throw new ValueCastError(schema, 'Array cast produced invalid data due to uniqueItems constraint') + return unique +} +function FromConstructor(schema: TConstructor, references: TSchema[], value: any): any { + if (Check(schema, references, value)) return Create(schema, references) + const required = new Set(schema.returns.required || []) + const result = function () {} + for (const [key, property] of Object.entries(schema.returns.properties)) { + if (!required.has(key) && value.prototype[key] === undefined) continue + result.prototype[key] = Visit(property as TSchema, references, value.prototype[key]) + } + return result +} +function FromImport(schema: TImport, references: TSchema[], value: unknown): boolean { + const definitions = globalThis.Object.values(schema.$defs) as TSchema[] + const target = schema.$defs[schema.$ref] as TSchema + return Visit(target, [...references, ...definitions], value) +} + +// ------------------------------------------------------------------ +// Intersect +// ------------------------------------------------------------------ +function IntersectAssign(correct: unknown, value: unknown): unknown { + // trust correct on mismatch | value on non-object + if ((IsObject(correct) && !IsObject(value)) || (!IsObject(correct) && IsObject(value))) return correct + if (!IsObject(correct) || !IsObject(value)) return value + return globalThis.Object.getOwnPropertyNames(correct).reduce((result, key) => { + const property = key in value ? IntersectAssign(correct[key], value[key]) : correct[key] + return { ...result, [key]: property } + }, {}) +} +function FromIntersect(schema: TIntersect, references: TSchema[], value: any): any { + if (Check(schema, references, value)) return value + const correct = Create(schema, references) + const assigned = IntersectAssign(correct, value) + return Check(schema, references, assigned) ? assigned : correct +} +function FromNever(schema: TNever, references: TSchema[], value: any): any { + throw new ValueCastError(schema, 'Never types cannot be cast') +} +function FromObject(schema: TObject, references: TSchema[], value: any): any { + if (Check(schema, references, value)) return value + if (value === null || typeof value !== 'object') return Create(schema, references) + const required = new Set(schema.required || []) + const result = {} as Record + for (const [key, property] of Object.entries(schema.properties)) { + if (!required.has(key) && value[key] === undefined) continue + result[key] = Visit(property, references, value[key]) + } + // additional schema properties + if (typeof schema.additionalProperties === 'object') { + const propertyNames = Object.getOwnPropertyNames(schema.properties) + for (const propertyName of Object.getOwnPropertyNames(value)) { + if (propertyNames.includes(propertyName)) continue + result[propertyName] = Visit(schema.additionalProperties, references, value[propertyName]) + } + } + return result +} +function FromRecord(schema: TRecord, references: TSchema[], value: any): any { + if (Check(schema, references, value)) return Clone(value) + if (value === null || typeof value !== 'object' || Array.isArray(value) || value instanceof Date) return Create(schema, references) + const subschemaPropertyName = Object.getOwnPropertyNames(schema.patternProperties)[0] + const subschema = schema.patternProperties[subschemaPropertyName] + const result = {} as Record + for (const [propKey, propValue] of Object.entries(value)) { + result[propKey] = Visit(subschema, references, propValue) + } + return result +} +function FromRef(schema: TRef, references: TSchema[], value: any): any { + return Visit(Deref(schema, references), references, value) +} +function FromThis(schema: TThis, references: TSchema[], value: any): any { + return Visit(Deref(schema, references), references, value) +} +function FromTuple(schema: TTuple, references: TSchema[], value: any): any { + if (Check(schema, references, value)) return Clone(value) + if (!IsArray(value)) return Create(schema, references) + if (schema.items === undefined) return [] + return schema.items.map((schema, index) => Visit(schema, references, value[index])) +} +function FromUnion(schema: TUnion, references: TSchema[], value: any): any { + return Check(schema, references, value) ? Clone(value) : CastUnion(schema, references, value) +} +function Visit(schema: TSchema, references: TSchema[], value: any): any { + const references_ = IsString(schema.$id) ? Pushref(schema, references) : references + const schema_ = schema as any + switch (schema[Kind]) { + // -------------------------------------------------------------- + // Structural + // -------------------------------------------------------------- + case 'Array': + return FromArray(schema_, references_, value) + case 'Constructor': + return FromConstructor(schema_, references_, value) + case 'Import': + return FromImport(schema_, references_, value) + case 'Intersect': + return FromIntersect(schema_, references_, value) + case 'Never': + return FromNever(schema_, references_, value) + case 'Object': + return FromObject(schema_, references_, value) + case 'Record': + return FromRecord(schema_, references_, value) + case 'Ref': + return FromRef(schema_, references_, value) + case 'This': + return FromThis(schema_, references_, value) + case 'Tuple': + return FromTuple(schema_, references_, value) + case 'Union': + return FromUnion(schema_, references_, value) + // -------------------------------------------------------------- + // DefaultClone + // -------------------------------------------------------------- + case 'Date': + case 'Symbol': + case 'Uint8Array': + return DefaultClone(schema, references, value) + // -------------------------------------------------------------- + // Default + // -------------------------------------------------------------- + default: + return Default(schema_, references_, value) + } +} +// ------------------------------------------------------------------ +// Cast +// ------------------------------------------------------------------ +/** Casts a value into a given type and references. The return value will retain as much information of the original value as possible. */ +export function Cast(schema: T, references: TSchema[], value: unknown): Static +/** Casts a value into a given type. The return value will retain as much information of the original value as possible. */ +export function Cast(schema: T, value: unknown): Static +/** Casts a value into a given type. The return value will retain as much information of the original value as possible. */ +export function Cast(...args: any[]) { + return args.length === 3 ? Visit(args[0], args[1], args[2]) : Visit(args[0], [], args[1]) +} diff --git a/src/value/cast/index.ts b/src/value/cast/index.ts new file mode 100644 index 000000000..9cdbd9556 --- /dev/null +++ b/src/value/cast/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/value + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './cast' diff --git a/src/value/check/check.ts b/src/value/check/check.ts new file mode 100644 index 000000000..7a61d9ad1 --- /dev/null +++ b/src/value/check/check.ts @@ -0,0 +1,513 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/value + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { TypeSystemPolicy } from '../../system/index' +import { Deref, Pushref } from '../deref/index' +import { Hash } from '../hash/index' +import { Kind } from '../../type/symbols/index' +import { KeyOfPattern } from '../../type/keyof/index' +import { ExtendsUndefinedCheck } from '../../type/extends/index' +import { TypeRegistry, FormatRegistry } from '../../type/registry/index' +import { TypeBoxError } from '../../type/error/index' + +import type { TSchema } from '../../type/schema/index' + +import type { TAny } from '../../type/any/index' +import type { TArgument } from '../../type/argument/index' +import type { TArray } from '../../type/array/index' +import type { TAsyncIterator } from '../../type/async-iterator/index' +import type { TBigInt } from '../../type/bigint/index' +import type { TBoolean } from '../../type/boolean/index' +import type { TDate } from '../../type/date/index' +import type { TConstructor } from '../../type/constructor/index' +import type { TFunction } from '../../type/function/index' +import type { TInteger } from '../../type/integer/index' +import type { TIntersect } from '../../type/intersect/index' +import type { TIterator } from '../../type/iterator/index' +import type { TLiteral } from '../../type/literal/index' +import type { TImport } from '../../type/module/index' +import { Never, type TNever } from '../../type/never/index' +import type { TNot } from '../../type/not/index' +import type { TNull } from '../../type/null/index' +import type { TNumber } from '../../type/number/index' +import type { TObject } from '../../type/object/index' +import type { TPromise } from '../../type/promise/index' +import type { TRecord } from '../../type/record/index' +import type { TRef } from '../../type/ref/index' +import type { TRegExp } from '../../type/regexp/index' +import type { TTemplateLiteral } from '../../type/template-literal/index' +import type { TThis } from '../../type/recursive/index' +import type { TTuple } from '../../type/tuple/index' +import type { TUnion } from '../../type/union/index' +import type { TUnknown } from '../../type/unknown/index' +import type { Static } from '../../type/static/index' +import type { TString } from '../../type/string/index' +import type { TSymbol } from '../../type/symbol/index' +import type { TUndefined } from '../../type/undefined/index' +import type { TUint8Array } from '../../type/uint8array/index' +import type { TVoid } from '../../type/void/index' + +// ------------------------------------------------------------------ +// ValueGuard +// ------------------------------------------------------------------ +import { IsArray, IsUint8Array, IsDate, IsPromise, IsFunction, IsAsyncIterator, IsIterator, IsBoolean, IsNumber, IsBigInt, IsString, IsSymbol, IsInteger, IsNull, IsUndefined } from '../guard/index' +// ------------------------------------------------------------------ +// KindGuard +// ------------------------------------------------------------------ +import { IsSchema } from '../../type/guard/kind' +// ------------------------------------------------------------------ +// Errors +// ------------------------------------------------------------------ +export class ValueCheckUnknownTypeError extends TypeBoxError { + constructor(public readonly schema: TSchema) { + super(`Unknown type`) + } +} +// ------------------------------------------------------------------ +// TypeGuards +// ------------------------------------------------------------------ +function IsAnyOrUnknown(schema: TSchema) { + return schema[Kind] === 'Any' || schema[Kind] === 'Unknown' +} +// ------------------------------------------------------------------ +// Guards +// ------------------------------------------------------------------ +function IsDefined(value: unknown): value is T { + return value !== undefined +} +// ------------------------------------------------------------------ +// Types +// ------------------------------------------------------------------ +function FromAny(schema: TAny, references: TSchema[], value: any): boolean { + return true +} +function FromArgument(schema: TArgument, references: TSchema[], value: any): boolean { + return true +} +function FromArray(schema: TArray, references: TSchema[], value: any): boolean { + if (!IsArray(value)) return false + if (IsDefined(schema.minItems) && !(value.length >= schema.minItems)) { + return false + } + if (IsDefined(schema.maxItems) && !(value.length <= schema.maxItems)) { + return false + } + if (!value.every((value) => Visit(schema.items, references, value))) { + return false + } + // prettier-ignore + if (schema.uniqueItems === true && !((function() { const set = new Set(); for(const element of value) { const hashed = Hash(element); if(set.has(hashed)) { return false } else { set.add(hashed) } } return true })())) { + return false + } + // contains + if (!(IsDefined(schema.contains) || IsNumber(schema.minContains) || IsNumber(schema.maxContains))) { + return true // exit + } + const containsSchema = IsDefined(schema.contains) ? schema.contains : Never() + const containsCount = value.reduce((acc: number, value) => (Visit(containsSchema, references, value) ? acc + 1 : acc), 0) + if (containsCount === 0) { + return false + } + if (IsNumber(schema.minContains) && containsCount < schema.minContains) { + return false + } + if (IsNumber(schema.maxContains) && containsCount > schema.maxContains) { + return false + } + return true +} +function FromAsyncIterator(schema: TAsyncIterator, references: TSchema[], value: any): boolean { + return IsAsyncIterator(value) +} +function FromBigInt(schema: TBigInt, references: TSchema[], value: any): boolean { + if (!IsBigInt(value)) return false + if (IsDefined(schema.exclusiveMaximum) && !(value < schema.exclusiveMaximum)) { + return false + } + if (IsDefined(schema.exclusiveMinimum) && !(value > schema.exclusiveMinimum)) { + return false + } + if (IsDefined(schema.maximum) && !(value <= schema.maximum)) { + return false + } + if (IsDefined(schema.minimum) && !(value >= schema.minimum)) { + return false + } + if (IsDefined(schema.multipleOf) && !(value % schema.multipleOf === BigInt(0))) { + return false + } + return true +} +function FromBoolean(schema: TBoolean, references: TSchema[], value: any): boolean { + return IsBoolean(value) +} +function FromConstructor(schema: TConstructor, references: TSchema[], value: any): boolean { + return Visit(schema.returns, references, value.prototype) +} +function FromDate(schema: TDate, references: TSchema[], value: any): boolean { + if (!IsDate(value)) return false + if (IsDefined(schema.exclusiveMaximumTimestamp) && !(value.getTime() < schema.exclusiveMaximumTimestamp)) { + return false + } + if (IsDefined(schema.exclusiveMinimumTimestamp) && !(value.getTime() > schema.exclusiveMinimumTimestamp)) { + return false + } + if (IsDefined(schema.maximumTimestamp) && !(value.getTime() <= schema.maximumTimestamp)) { + return false + } + if (IsDefined(schema.minimumTimestamp) && !(value.getTime() >= schema.minimumTimestamp)) { + return false + } + if (IsDefined(schema.multipleOfTimestamp) && !(value.getTime() % schema.multipleOfTimestamp === 0)) { + return false + } + return true +} +function FromFunction(schema: TFunction, references: TSchema[], value: any): boolean { + return IsFunction(value) +} +function FromImport(schema: TImport, references: TSchema[], value: any): boolean { + const definitions = globalThis.Object.values(schema.$defs) as TSchema[] + const target = schema.$defs[schema.$ref] as TSchema + return Visit(target, [...references, ...definitions], value) +} +function FromInteger(schema: TInteger, references: TSchema[], value: any): boolean { + if (!IsInteger(value)) { + return false + } + if (IsDefined(schema.exclusiveMaximum) && !(value < schema.exclusiveMaximum)) { + return false + } + if (IsDefined(schema.exclusiveMinimum) && !(value > schema.exclusiveMinimum)) { + return false + } + if (IsDefined(schema.maximum) && !(value <= schema.maximum)) { + return false + } + if (IsDefined(schema.minimum) && !(value >= schema.minimum)) { + return false + } + if (IsDefined(schema.multipleOf) && !(value % schema.multipleOf === 0)) { + return false + } + return true +} +function FromIntersect(schema: TIntersect, references: TSchema[], value: any): boolean { + const check1 = schema.allOf.every((schema) => Visit(schema, references, value)) + if (schema.unevaluatedProperties === false) { + const keyPattern = new RegExp(KeyOfPattern(schema)) + const check2 = Object.getOwnPropertyNames(value).every((key) => keyPattern.test(key)) + return check1 && check2 + } else if (IsSchema(schema.unevaluatedProperties)) { + const keyCheck = new RegExp(KeyOfPattern(schema)) + const check2 = Object.getOwnPropertyNames(value).every((key) => keyCheck.test(key) || Visit(schema.unevaluatedProperties as TSchema, references, value[key])) + return check1 && check2 + } else { + return check1 + } +} +function FromIterator(schema: TIterator, references: TSchema[], value: any): boolean { + return IsIterator(value) +} +function FromLiteral(schema: TLiteral, references: TSchema[], value: any): boolean { + return value === schema.const +} +function FromNever(schema: TNever, references: TSchema[], value: any): boolean { + return false +} +function FromNot(schema: TNot, references: TSchema[], value: any): boolean { + return !Visit(schema.not, references, value) +} +function FromNull(schema: TNull, references: TSchema[], value: any): boolean { + return IsNull(value) +} +function FromNumber(schema: TNumber, references: TSchema[], value: any): boolean { + if (!TypeSystemPolicy.IsNumberLike(value)) return false + if (IsDefined(schema.exclusiveMaximum) && !(value < schema.exclusiveMaximum)) { + return false + } + if (IsDefined(schema.exclusiveMinimum) && !(value > schema.exclusiveMinimum)) { + return false + } + if (IsDefined(schema.minimum) && !(value >= schema.minimum)) { + return false + } + if (IsDefined(schema.maximum) && !(value <= schema.maximum)) { + return false + } + if (IsDefined(schema.multipleOf) && !(value % schema.multipleOf === 0)) { + return false + } + return true +} +function FromObject(schema: TObject, references: TSchema[], value: any): boolean { + if (!TypeSystemPolicy.IsObjectLike(value)) return false + if (IsDefined(schema.minProperties) && !(Object.getOwnPropertyNames(value).length >= schema.minProperties)) { + return false + } + if (IsDefined(schema.maxProperties) && !(Object.getOwnPropertyNames(value).length <= schema.maxProperties)) { + return false + } + const knownKeys = Object.getOwnPropertyNames(schema.properties) + for (const knownKey of knownKeys) { + const property = schema.properties[knownKey] + if (schema.required && schema.required.includes(knownKey)) { + if (!Visit(property, references, value[knownKey])) { + return false + } + if ((ExtendsUndefinedCheck(property) || IsAnyOrUnknown(property)) && !(knownKey in value)) { + return false + } + } else { + if (TypeSystemPolicy.IsExactOptionalProperty(value, knownKey) && !Visit(property, references, value[knownKey])) { + return false + } + } + } + if (schema.additionalProperties === false) { + const valueKeys = Object.getOwnPropertyNames(value) + // optimization: value is valid if schemaKey length matches the valueKey length + if (schema.required && schema.required.length === knownKeys.length && valueKeys.length === knownKeys.length) { + return true + } else { + return valueKeys.every((valueKey) => knownKeys.includes(valueKey)) + } + } else if (typeof schema.additionalProperties === 'object') { + const valueKeys = Object.getOwnPropertyNames(value) + return valueKeys.every((key) => knownKeys.includes(key) || Visit(schema.additionalProperties as TSchema, references, value[key])) + } else { + return true + } +} +function FromPromise(schema: TPromise, references: TSchema[], value: any): boolean { + return IsPromise(value) +} +function FromRecord(schema: TRecord, references: TSchema[], value: any): boolean { + if (!TypeSystemPolicy.IsRecordLike(value)) { + return false + } + if (IsDefined(schema.minProperties) && !(Object.getOwnPropertyNames(value).length >= schema.minProperties)) { + return false + } + if (IsDefined(schema.maxProperties) && !(Object.getOwnPropertyNames(value).length <= schema.maxProperties)) { + return false + } + const [patternKey, patternSchema] = Object.entries(schema.patternProperties)[0] + const regex = new RegExp(patternKey) + // prettier-ignore + const check1 = Object.entries(value).every(([key, value]) => { + return (regex.test(key)) ? Visit(patternSchema, references, value) : true + }) + // prettier-ignore + const check2 = typeof schema.additionalProperties === 'object' ? Object.entries(value).every(([key, value]) => { + return (!regex.test(key)) ? Visit(schema.additionalProperties as TSchema, references, value) : true + }) : true + const check3 = + schema.additionalProperties === false + ? Object.getOwnPropertyNames(value).every((key) => { + return regex.test(key) + }) + : true + return check1 && check2 && check3 +} +function FromRef(schema: TRef, references: TSchema[], value: any): boolean { + return Visit(Deref(schema, references), references, value) +} +function FromRegExp(schema: TRegExp, references: TSchema[], value: any): boolean { + const regex = new RegExp(schema.source, schema.flags) + if (IsDefined(schema.minLength)) { + if (!(value.length >= schema.minLength)) return false + } + if (IsDefined(schema.maxLength)) { + if (!(value.length <= schema.maxLength)) return false + } + return regex.test(value) +} +function FromString(schema: TString, references: TSchema[], value: any): boolean { + if (!IsString(value)) { + return false + } + if (IsDefined(schema.minLength)) { + if (!(value.length >= schema.minLength)) return false + } + if (IsDefined(schema.maxLength)) { + if (!(value.length <= schema.maxLength)) return false + } + if (IsDefined(schema.pattern)) { + const regex = new RegExp(schema.pattern) + if (!regex.test(value)) return false + } + if (IsDefined(schema.format)) { + if (!FormatRegistry.Has(schema.format)) return false + const func = FormatRegistry.Get(schema.format)! + return func(value) + } + return true +} +function FromSymbol(schema: TSymbol, references: TSchema[], value: any): boolean { + return IsSymbol(value) +} +function FromTemplateLiteral(schema: TTemplateLiteral, references: TSchema[], value: any): boolean { + return IsString(value) && new RegExp(schema.pattern).test(value) +} +function FromThis(schema: TThis, references: TSchema[], value: any): boolean { + return Visit(Deref(schema, references), references, value) +} +function FromTuple(schema: TTuple, references: TSchema[], value: any): boolean { + if (!IsArray(value)) { + return false + } + if (schema.items === undefined && !(value.length === 0)) { + return false + } + if (!(value.length === schema.maxItems)) { + return false + } + if (!schema.items) { + return true + } + for (let i = 0; i < schema.items.length; i++) { + if (!Visit(schema.items[i], references, value[i])) return false + } + return true +} +function FromUndefined(schema: TUndefined, references: TSchema[], value: any): boolean { + return IsUndefined(value) +} +function FromUnion(schema: TUnion, references: TSchema[], value: any): boolean { + return schema.anyOf.some((inner) => Visit(inner, references, value)) +} +function FromUint8Array(schema: TUint8Array, references: TSchema[], value: any): boolean { + if (!IsUint8Array(value)) { + return false + } + if (IsDefined(schema.maxByteLength) && !(value.length <= schema.maxByteLength)) { + return false + } + if (IsDefined(schema.minByteLength) && !(value.length >= schema.minByteLength)) { + return false + } + return true +} +function FromUnknown(schema: TUnknown, references: TSchema[], value: any): boolean { + return true +} +function FromVoid(schema: TVoid, references: TSchema[], value: any): boolean { + return TypeSystemPolicy.IsVoidLike(value) +} +function FromKind(schema: TSchema, references: TSchema[], value: unknown): boolean { + if (!TypeRegistry.Has(schema[Kind])) return false + const func = TypeRegistry.Get(schema[Kind])! + return func(schema, value) +} +function Visit(schema: T, references: TSchema[], value: any): boolean { + const references_ = IsDefined(schema.$id) ? Pushref(schema, references) : references + const schema_ = schema as any + switch (schema_[Kind]) { + case 'Any': + return FromAny(schema_, references_, value) + case 'Argument': + return FromArgument(schema_, references_, value) + case 'Array': + return FromArray(schema_, references_, value) + case 'AsyncIterator': + return FromAsyncIterator(schema_, references_, value) + case 'BigInt': + return FromBigInt(schema_, references_, value) + case 'Boolean': + return FromBoolean(schema_, references_, value) + case 'Constructor': + return FromConstructor(schema_, references_, value) + case 'Date': + return FromDate(schema_, references_, value) + case 'Function': + return FromFunction(schema_, references_, value) + case 'Import': + return FromImport(schema_, references_, value) + case 'Integer': + return FromInteger(schema_, references_, value) + case 'Intersect': + return FromIntersect(schema_, references_, value) + case 'Iterator': + return FromIterator(schema_, references_, value) + case 'Literal': + return FromLiteral(schema_, references_, value) + case 'Never': + return FromNever(schema_, references_, value) + case 'Not': + return FromNot(schema_, references_, value) + case 'Null': + return FromNull(schema_, references_, value) + case 'Number': + return FromNumber(schema_, references_, value) + case 'Object': + return FromObject(schema_, references_, value) + case 'Promise': + return FromPromise(schema_, references_, value) + case 'Record': + return FromRecord(schema_, references_, value) + case 'Ref': + return FromRef(schema_, references_, value) + case 'RegExp': + return FromRegExp(schema_, references_, value) + case 'String': + return FromString(schema_, references_, value) + case 'Symbol': + return FromSymbol(schema_, references_, value) + case 'TemplateLiteral': + return FromTemplateLiteral(schema_, references_, value) + case 'This': + return FromThis(schema_, references_, value) + case 'Tuple': + return FromTuple(schema_, references_, value) + case 'Undefined': + return FromUndefined(schema_, references_, value) + case 'Union': + return FromUnion(schema_, references_, value) + case 'Uint8Array': + return FromUint8Array(schema_, references_, value) + case 'Unknown': + return FromUnknown(schema_, references_, value) + case 'Void': + return FromVoid(schema_, references_, value) + default: + if (!TypeRegistry.Has(schema_[Kind])) throw new ValueCheckUnknownTypeError(schema_) + return FromKind(schema_, references_, value) + } +} +// -------------------------------------------------------------------------- +// Check +// -------------------------------------------------------------------------- +/** Returns true if the value matches the given type. */ +export function Check(schema: T, references: TSchema[], value: unknown): value is Static +/** Returns true if the value matches the given type. */ +export function Check(schema: T, value: unknown): value is Static +/** Returns true if the value matches the given type. */ +export function Check(...args: any[]) { + return args.length === 3 ? Visit(args[0], args[1], args[2]) : Visit(args[0], [], args[1]) +} diff --git a/src/value/check/index.ts b/src/value/check/index.ts new file mode 100644 index 000000000..9fe61e89e --- /dev/null +++ b/src/value/check/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/value + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './check' diff --git a/src/value/clean/clean.ts b/src/value/clean/clean.ts new file mode 100644 index 000000000..6a692e347 --- /dev/null +++ b/src/value/clean/clean.ts @@ -0,0 +1,194 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/value + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { KeyOfPropertyKeys } from '../../type/keyof/index' +import { Check } from '../check/index' +import { Clone } from '../clone/index' +import { Deref, Pushref } from '../deref/index' +import { Kind } from '../../type/symbols/index' + +import type { TSchema } from '../../type/schema/index' +import type { TArray } from '../../type/array/index' +import type { TImport } from '../../type/module/index' +import type { TIntersect } from '../../type/intersect/index' +import type { TObject } from '../../type/object/index' +import type { TRecord } from '../../type/record/index' +import type { TRef } from '../../type/ref/index' +import type { TThis } from '../../type/recursive/index' +import type { TTuple } from '../../type/tuple/index' +import type { TUnion } from '../../type/union/index' + +// ------------------------------------------------------------------ +// ValueGuard +// ------------------------------------------------------------------ +// prettier-ignore +import { + HasPropertyKey, + IsString, + IsObject, + IsArray, + IsUndefined +} from '../guard/index' +// ------------------------------------------------------------------ +// TypeGuard +// ------------------------------------------------------------------ +// prettier-ignore +import { + IsKind +} from '../../type/guard/kind' + +// ------------------------------------------------------------------ +// IsCheckable +// ------------------------------------------------------------------ +function IsCheckable(schema: unknown): boolean { + return IsKind(schema) && schema[Kind] !== 'Unsafe' +} +// ------------------------------------------------------------------ +// Types +// ------------------------------------------------------------------ +function FromArray(schema: TArray, references: TSchema[], value: unknown): any { + if (!IsArray(value)) return value + return value.map((value) => Visit(schema.items, references, value)) +} +function FromImport(schema: TImport, references: TSchema[], value: unknown): any { + const definitions = globalThis.Object.values(schema.$defs) as TSchema[] + const target = schema.$defs[schema.$ref] as TSchema + return Visit(target, [...references, ...definitions], value) +} +function FromIntersect(schema: TIntersect, references: TSchema[], value: unknown): any { + const unevaluatedProperties = schema.unevaluatedProperties as TSchema + const intersections = schema.allOf.map((schema) => Visit(schema, references, Clone(value))) + const composite = intersections.reduce((acc: any, value: any) => (IsObject(value) ? { ...acc, ...value } : value), {}) + if (!IsObject(value) || !IsObject(composite) || !IsKind(unevaluatedProperties)) return composite + const knownkeys = KeyOfPropertyKeys(schema) as string[] + for (const key of Object.getOwnPropertyNames(value)) { + if (knownkeys.includes(key)) continue + if (Check(unevaluatedProperties, references, value[key])) { + composite[key] = Visit(unevaluatedProperties, references, value[key]) + } + } + return composite +} +function FromObject(schema: TObject, references: TSchema[], value: unknown): any { + if (!IsObject(value) || IsArray(value)) return value // Check IsArray for AllowArrayObject configuration + const additionalProperties = schema.additionalProperties as TSchema + for (const key of Object.getOwnPropertyNames(value)) { + if (HasPropertyKey(schema.properties, key)) { + value[key] = Visit(schema.properties[key], references, value[key]) + continue + } + if (IsKind(additionalProperties) && Check(additionalProperties, references, value[key])) { + value[key] = Visit(additionalProperties, references, value[key]) + continue + } + delete value[key] + } + return value +} +function FromRecord(schema: TRecord, references: TSchema[], value: unknown): any { + if (!IsObject(value)) return value + const additionalProperties = schema.additionalProperties as TSchema + const propertyKeys = Object.getOwnPropertyNames(value) + const [propertyKey, propertySchema] = Object.entries(schema.patternProperties)[0] + const propertyKeyTest = new RegExp(propertyKey) + for (const key of propertyKeys) { + if (propertyKeyTest.test(key)) { + value[key] = Visit(propertySchema, references, value[key]) + continue + } + if (IsKind(additionalProperties) && Check(additionalProperties, references, value[key])) { + value[key] = Visit(additionalProperties, references, value[key]) + continue + } + delete value[key] + } + return value +} +function FromRef(schema: TRef, references: TSchema[], value: unknown): any { + return Visit(Deref(schema, references), references, value) +} +function FromThis(schema: TThis, references: TSchema[], value: unknown): any { + return Visit(Deref(schema, references), references, value) +} +function FromTuple(schema: TTuple, references: TSchema[], value: unknown): any { + if (!IsArray(value)) return value + if (IsUndefined(schema.items)) return [] + const length = Math.min(value.length, schema.items.length) + for (let i = 0; i < length; i++) { + value[i] = Visit(schema.items[i], references, value[i]) + } + // prettier-ignore + return value.length > length + ? value.slice(0, length) + : value +} +function FromUnion(schema: TUnion, references: TSchema[], value: unknown): any { + for (const inner of schema.anyOf) { + if (IsCheckable(inner) && Check(inner, references, value)) { + return Visit(inner, references, value) + } + } + return value +} +function Visit(schema: TSchema, references: TSchema[], value: unknown): unknown { + const references_ = IsString(schema.$id) ? Pushref(schema, references) : references + const schema_ = schema as any + switch (schema_[Kind]) { + case 'Array': + return FromArray(schema_, references_, value) + case 'Import': + return FromImport(schema_, references_, value) + case 'Intersect': + return FromIntersect(schema_, references_, value) + case 'Object': + return FromObject(schema_, references_, value) + case 'Record': + return FromRecord(schema_, references_, value) + case 'Ref': + return FromRef(schema_, references_, value) + case 'This': + return FromThis(schema_, references_, value) + case 'Tuple': + return FromTuple(schema_, references_, value) + case 'Union': + return FromUnion(schema_, references_, value) + default: + return value + } +} +// ------------------------------------------------------------------ +// Clean +// ------------------------------------------------------------------ +/** `[Mutable]` Removes excess properties from a value and returns the result. This function does not check the value and returns an unknown type. You should Check the result before use. Clean is a mutable operation. To avoid mutation, Clone the value first. */ +export function Clean(schema: TSchema, references: TSchema[], value: unknown): unknown +/** `[Mutable]` Removes excess properties from a value and returns the result. This function does not check the value and returns an unknown type. You should Check the result before use. Clean is a mutable operation. To avoid mutation, Clone the value first. */ +export function Clean(schema: TSchema, value: unknown): unknown +/** `[Mutable]` Removes excess properties from a value and returns the result. This function does not check the value and returns an unknown type. You should Check the result before use. Clean is a mutable operation. To avoid mutation, Clone the value first. */ +export function Clean(...args: any[]) { + return args.length === 3 ? Visit(args[0], args[1], args[2]) : Visit(args[0], [], args[1]) +} diff --git a/src/value/clean/index.ts b/src/value/clean/index.ts new file mode 100644 index 000000000..89ecef46e --- /dev/null +++ b/src/value/clean/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/value + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './clean' diff --git a/src/value/clone/clone.ts b/src/value/clone/clone.ts new file mode 100644 index 000000000..7ea2261b4 --- /dev/null +++ b/src/value/clone/clone.ts @@ -0,0 +1,79 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/value + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { ObjectType as FromObject, ArrayType as FromArray, TypedArrayType, ValueType } from '../guard/index' + +// ------------------------------------------------------------------ +// ValueGuard +// ------------------------------------------------------------------ +import { IsArray, IsDate, IsMap, IsSet, IsObject, IsTypedArray, IsValueType } from '../guard/index' +// ------------------------------------------------------------------ +// Clonable +// ------------------------------------------------------------------ +function FromObject(value: FromObject): any { + const Acc = {} as Record + for (const key of Object.getOwnPropertyNames(value)) { + Acc[key] = Clone(value[key]) + } + for (const key of Object.getOwnPropertySymbols(value)) { + Acc[key] = Clone(value[key]) + } + return Acc +} +function FromArray(value: FromArray): any { + return value.map((element: any) => Clone(element)) +} +function FromTypedArray(value: TypedArrayType): any { + return value.slice() +} +function FromMap(value: Map): any { + return new Map(Clone([...value.entries()])) +} +function FromSet(value: Set): any { + return new Set(Clone([...value.entries()])) +} +function FromDate(value: Date): any { + return new Date(value.toISOString()) +} +function FromValue(value: ValueType): any { + return value +} +// ------------------------------------------------------------------ +// Clone +// ------------------------------------------------------------------ +/** Returns a clone of the given value */ +export function Clone(value: T): T { + if (IsArray(value)) return FromArray(value) + if (IsDate(value)) return FromDate(value) + if (IsTypedArray(value)) return FromTypedArray(value) + if (IsMap(value)) return FromMap(value) + if (IsSet(value)) return FromSet(value) + if (IsObject(value)) return FromObject(value) + if (IsValueType(value)) return FromValue(value) + throw new Error('ValueClone: Unable to clone value') +} diff --git a/src/value/clone/index.ts b/src/value/clone/index.ts new file mode 100644 index 000000000..c6563b3cb --- /dev/null +++ b/src/value/clone/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/value + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './clone' diff --git a/src/value/convert/convert.ts b/src/value/convert/convert.ts new file mode 100644 index 000000000..b23c0bba8 --- /dev/null +++ b/src/value/convert/convert.ts @@ -0,0 +1,318 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/value + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { Clone } from '../clone/index' +import { Check } from '../check/index' +import { Deref, Pushref } from '../deref/index' +import { Kind } from '../../type/symbols/index' + +import type { TSchema } from '../../type/schema/index' +import type { TArray } from '../../type/array/index' +import type { TBigInt } from '../../type/bigint/index' +import type { TBoolean } from '../../type/boolean/index' +import type { TDate } from '../../type/date/index' +import type { TInteger } from '../../type/integer/index' +import type { TIntersect } from '../../type/intersect/index' +import type { TImport } from '../../type/module/index' +import type { TLiteral } from '../../type/literal/index' +import type { TNull } from '../../type/null/index' +import type { TNumber } from '../../type/number/index' +import type { TObject } from '../../type/object/index' +import type { TRecord } from '../../type/record/index' +import type { TRef } from '../../type/ref/index' +import type { TThis } from '../../type/recursive/index' +import type { TTuple } from '../../type/tuple/index' +import type { TUnion } from '../../type/union/index' +import type { TString } from '../../type/string/index' +import type { TSymbol } from '../../type/symbol/index' +import type { TUndefined } from '../../type/undefined/index' + +// ------------------------------------------------------------------ +// ValueGuard +// ------------------------------------------------------------------ +import { IsArray, IsObject, IsDate, IsUndefined, IsString, IsNumber, IsBoolean, IsBigInt, IsSymbol, HasPropertyKey } from '../guard/index' + +// ------------------------------------------------------------------ +// Conversions +// ------------------------------------------------------------------ +function IsStringNumeric(value: unknown): value is string { + return IsString(value) && !isNaN(value as any) && !isNaN(parseFloat(value)) +} +function IsValueToString(value: unknown): value is { toString: () => string } { + return IsBigInt(value) || IsBoolean(value) || IsNumber(value) +} +function IsValueTrue(value: unknown): value is true { + return value === true || (IsNumber(value) && value === 1) || (IsBigInt(value) && value === BigInt('1')) || (IsString(value) && (value.toLowerCase() === 'true' || value === '1')) +} +function IsValueFalse(value: unknown): value is false { + return value === false || (IsNumber(value) && (value === 0 || Object.is(value, -0))) || (IsBigInt(value) && value === BigInt('0')) || (IsString(value) && (value.toLowerCase() === 'false' || value === '0' || value === '-0')) +} +function IsTimeStringWithTimeZone(value: unknown): value is string { + return IsString(value) && /^(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)$/i.test(value) +} +function IsTimeStringWithoutTimeZone(value: unknown): value is string { + return IsString(value) && /^(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)?$/i.test(value) +} +function IsDateTimeStringWithTimeZone(value: unknown): value is string { + return IsString(value) && /^\d\d\d\d-[0-1]\d-[0-3]\dt(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)$/i.test(value) +} +function IsDateTimeStringWithoutTimeZone(value: unknown): value is string { + return IsString(value) && /^\d\d\d\d-[0-1]\d-[0-3]\dt(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)?$/i.test(value) +} +function IsDateString(value: unknown): value is string { + return IsString(value) && /^\d\d\d\d-[0-1]\d-[0-3]\d$/i.test(value) +} +// ------------------------------------------------------------------ +// Convert +// ------------------------------------------------------------------ +function TryConvertLiteralString(value: unknown, target: string) { + const conversion = TryConvertString(value) + return conversion === target ? conversion : value +} +function TryConvertLiteralNumber(value: unknown, target: number) { + const conversion = TryConvertNumber(value) + return conversion === target ? conversion : value +} +function TryConvertLiteralBoolean(value: unknown, target: boolean) { + const conversion = TryConvertBoolean(value) + return conversion === target ? conversion : value +} +// prettier-ignore +function TryConvertLiteral(schema: TLiteral, value: unknown) { + return ( + IsString(schema.const) ? TryConvertLiteralString(value, schema.const) : + IsNumber(schema.const) ? TryConvertLiteralNumber(value, schema.const) : + IsBoolean(schema.const) ? TryConvertLiteralBoolean(value, schema.const) : + value + ) +} +function TryConvertBoolean(value: unknown) { + return IsValueTrue(value) ? true : IsValueFalse(value) ? false : value +} +function TryConvertBigInt(value: unknown) { + const truncateInteger = (value: string) => value.split('.')[0] + return IsStringNumeric(value) ? BigInt(truncateInteger(value)) : IsNumber(value) ? BigInt(Math.trunc(value)) : IsValueFalse(value) ? BigInt(0) : IsValueTrue(value) ? BigInt(1) : value +} +function TryConvertString(value: unknown) { + return IsSymbol(value) && value.description !== undefined ? value.description.toString() : IsValueToString(value) ? value.toString() : value +} +function TryConvertNumber(value: unknown) { + return IsStringNumeric(value) ? parseFloat(value) : IsValueTrue(value) ? 1 : IsValueFalse(value) ? 0 : value +} +function TryConvertInteger(value: unknown) { + return IsStringNumeric(value) ? parseInt(value) : IsNumber(value) ? Math.trunc(value) : IsValueTrue(value) ? 1 : IsValueFalse(value) ? 0 : value +} +function TryConvertNull(value: unknown) { + return IsString(value) && value.toLowerCase() === 'null' ? null : value +} +function TryConvertUndefined(value: unknown) { + return IsString(value) && value === 'undefined' ? undefined : value +} +// ------------------------------------------------------------------ +// note: this function may return an invalid dates for the regex +// tests above. Invalid dates will however be checked during the +// casting function and will return a epoch date if invalid. +// Consider better string parsing for the iso dates in future +// revisions. +// ------------------------------------------------------------------ +// prettier-ignore +function TryConvertDate(value: unknown) { + return ( + IsDate(value) ? value : + IsNumber(value) ? new Date(value) : + IsValueTrue(value) ? new Date(1) : + IsValueFalse(value) ? new Date(0) : + IsStringNumeric(value) ? new Date(parseInt(value)) : + IsTimeStringWithoutTimeZone(value) ? new Date(`1970-01-01T${value}.000Z`) : + IsTimeStringWithTimeZone(value) ? new Date(`1970-01-01T${value}`) : + IsDateTimeStringWithoutTimeZone(value) ? new Date(`${value}.000Z`) : + IsDateTimeStringWithTimeZone(value) ? new Date(value) : + IsDateString(value) ? new Date(`${value}T00:00:00.000Z`) : + value + ) +} +// ------------------------------------------------------------------ +// Default +// ------------------------------------------------------------------ +function Default(value: unknown): unknown { + return value +} +// ------------------------------------------------------------------ +// Convert +// ------------------------------------------------------------------ +function FromArray(schema: TArray, references: TSchema[], value: any): any { + const elements = IsArray(value) ? value : [value] + return elements.map((element) => Visit(schema.items, references, element)) +} +function FromBigInt(schema: TBigInt, references: TSchema[], value: any): unknown { + return TryConvertBigInt(value) +} +function FromBoolean(schema: TBoolean, references: TSchema[], value: any): unknown { + return TryConvertBoolean(value) +} +function FromDate(schema: TDate, references: TSchema[], value: any): unknown { + return TryConvertDate(value) +} +function FromImport(schema: TImport, references: TSchema[], value: unknown): unknown { + const definitions = globalThis.Object.values(schema.$defs) as TSchema[] + const target = schema.$defs[schema.$ref] as TSchema + return Visit(target, [...references, ...definitions], value) +} +function FromInteger(schema: TInteger, references: TSchema[], value: any): unknown { + return TryConvertInteger(value) +} +function FromIntersect(schema: TIntersect, references: TSchema[], value: any): unknown { + return schema.allOf.reduce((value, schema) => Visit(schema, references, value), value) +} +function FromLiteral(schema: TLiteral, references: TSchema[], value: any): unknown { + return TryConvertLiteral(schema, value) +} +function FromNull(schema: TNull, references: TSchema[], value: any): unknown { + return TryConvertNull(value) +} +function FromNumber(schema: TNumber, references: TSchema[], value: any): unknown { + return TryConvertNumber(value) +} +// prettier-ignore +function FromObject(schema: TObject, references: TSchema[], value: any): unknown { + if(!IsObject(value) || IsArray(value)) return value + for(const propertyKey of Object.getOwnPropertyNames(schema.properties)) { + if(!HasPropertyKey(value, propertyKey)) continue + value[propertyKey] = Visit(schema.properties[propertyKey], references, value[propertyKey]) + } + return value +} +function FromRecord(schema: TRecord, references: TSchema[], value: any): unknown { + const isConvertable = IsObject(value) && !IsArray(value) + if (!isConvertable) return value + const propertyKey = Object.getOwnPropertyNames(schema.patternProperties)[0] + const property = schema.patternProperties[propertyKey] + for (const [propKey, propValue] of Object.entries(value)) { + value[propKey] = Visit(property, references, propValue) + } + return value +} +function FromRef(schema: TRef, references: TSchema[], value: any): unknown { + return Visit(Deref(schema, references), references, value) +} +function FromString(schema: TString, references: TSchema[], value: any): unknown { + return TryConvertString(value) +} +function FromSymbol(schema: TSymbol, references: TSchema[], value: any): unknown { + return IsString(value) || IsNumber(value) ? Symbol(value) : value +} +function FromThis(schema: TThis, references: TSchema[], value: any): unknown { + return Visit(Deref(schema, references), references, value) +} +// prettier-ignore +function FromTuple(schema: TTuple, references: TSchema[], value: any): unknown { + const isConvertable = IsArray(value) && !IsUndefined(schema.items) + if(!isConvertable) return value + return value.map((value, index) => { + return (index < schema.items!.length) + ? Visit(schema.items![index], references, value) + : value + }) +} +function FromUndefined(schema: TUndefined, references: TSchema[], value: any): unknown { + return TryConvertUndefined(value) +} +function FromUnion(schema: TUnion, references: TSchema[], value: any): unknown { + // Check if original value already matches one of the union variants + for (const subschema of schema.anyOf) { + if (Check(subschema, references, value)) { + return value + } + } + + // Attempt conversion for each variant + for (const subschema of schema.anyOf) { + const converted = Visit(subschema, references, Clone(value)) + if (!Check(subschema, references, converted)) continue + return converted + } + return value +} +function Visit(schema: TSchema, references: TSchema[], value: any): unknown { + const references_ = Pushref(schema, references) + const schema_ = schema as any + switch (schema[Kind]) { + case 'Array': + return FromArray(schema_, references_, value) + case 'BigInt': + return FromBigInt(schema_, references_, value) + case 'Boolean': + return FromBoolean(schema_, references_, value) + case 'Date': + return FromDate(schema_, references_, value) + case 'Import': + return FromImport(schema_, references_, value) + case 'Integer': + return FromInteger(schema_, references_, value) + case 'Intersect': + return FromIntersect(schema_, references_, value) + case 'Literal': + return FromLiteral(schema_, references_, value) + case 'Null': + return FromNull(schema_, references_, value) + case 'Number': + return FromNumber(schema_, references_, value) + case 'Object': + return FromObject(schema_, references_, value) + case 'Record': + return FromRecord(schema_, references_, value) + case 'Ref': + return FromRef(schema_, references_, value) + case 'String': + return FromString(schema_, references_, value) + case 'Symbol': + return FromSymbol(schema_, references_, value) + case 'This': + return FromThis(schema_, references_, value) + case 'Tuple': + return FromTuple(schema_, references_, value) + case 'Undefined': + return FromUndefined(schema_, references_, value) + case 'Union': + return FromUnion(schema_, references_, value) + default: + return Default(value) + } +} +// ------------------------------------------------------------------ +// Convert +// ------------------------------------------------------------------ +/** `[Mutable]` Converts any type mismatched values to their target type if a reasonable conversion is possible. */ +export function Convert(schema: TSchema, references: TSchema[], value: unknown): unknown +/** `[Mutable]` Converts any type mismatched values to their target type if a reasonable conversion is possible. */ +export function Convert(schema: TSchema, value: unknown): unknown +/** `[Mutable]` Converts any type mismatched values to their target type if a reasonable conversion is possible. */ +// prettier-ignore +export function Convert(...args: any[]) { + return args.length === 3 ? Visit(args[0], args[1], args[2]) : Visit(args[0], [], args[1]) +} diff --git a/src/value/convert/index.ts b/src/value/convert/index.ts new file mode 100644 index 000000000..9ff2227d1 --- /dev/null +++ b/src/value/convert/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/value + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './convert' diff --git a/src/value/create/create.ts b/src/value/create/create.ts new file mode 100644 index 000000000..9c75b0d50 --- /dev/null +++ b/src/value/create/create.ts @@ -0,0 +1,488 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/value + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { HasPropertyKey } from '../guard/index' +import { Check } from '../check/index' +import { Clone } from '../clone/index' +import { Deref, Pushref } from '../deref/index' +import { TemplateLiteralGenerate, IsTemplateLiteralFinite } from '../../type/template-literal/index' +import { PatternStringExact, PatternNumberExact } from '../../type/patterns/index' +import { TypeRegistry } from '../../type/registry/index' +import { Kind } from '../../type/symbols/index' +import { TypeBoxError } from '../../type/error/index' + +import type { TSchema } from '../../type/schema/index' +import type { TAsyncIterator } from '../../type/async-iterator/index' +import type { TAny } from '../../type/any/index' +import type { TArray } from '../../type/array/index' +import type { TBigInt } from '../../type/bigint/index' +import type { TBoolean } from '../../type/boolean/index' +import type { TDate } from '../../type/date/index' +import type { TConstructor } from '../../type/constructor/index' +import type { TFunction } from '../../type/function/index' +import type { TImport } from '../../type/module/index' +import type { TInteger } from '../../type/integer/index' +import type { TIntersect } from '../../type/intersect/index' +import type { TIterator } from '../../type/iterator/index' +import type { TLiteral } from '../../type/literal/index' +import type { TNever } from '../../type/never/index' +import type { TNot } from '../../type/not/index' +import type { TNull } from '../../type/null/index' +import type { TNumber } from '../../type/number/index' +import type { TObject } from '../../type/object/index' +import type { TPromise } from '../../type/promise/index' +import type { TRecord } from '../../type/record/index' +import type { TRef } from '../../type/ref/index' +import type { TRegExp } from '../../type/regexp/index' +import type { TTemplateLiteral } from '../../type/template-literal/index' +import type { TThis } from '../../type/recursive/index' +import type { TTuple } from '../../type/tuple/index' +import type { TUnion } from '../../type/union/index' +import type { TUnknown } from '../../type/unknown/index' +import type { Static } from '../../type/static/index' +import type { TString } from '../../type/string/index' +import type { TSymbol } from '../../type/symbol/index' +import type { TUndefined } from '../../type/undefined/index' +import type { TUint8Array } from '../../type/uint8array/index' +import type { TVoid } from '../../type/void/index' + +import { IsFunction } from '../guard/guard' + +// ------------------------------------------------------------------ +// Errors +// ------------------------------------------------------------------ +export class ValueCreateError extends TypeBoxError { + constructor(public readonly schema: TSchema, message: string) { + super(message) + } +} +// ------------------------------------------------------------------ +// Default +// ------------------------------------------------------------------ +function FromDefault(value: unknown) { + return IsFunction(value) ? value() : Clone(value) +} +// ------------------------------------------------------------------ +// Create +// ------------------------------------------------------------------ +function FromAny(schema: TAny, references: TSchema[]): any { + if (HasPropertyKey(schema, 'default')) { + return FromDefault(schema.default) + } else { + return {} + } +} +function FromArgument(schema: TAny, references: TSchema[]): any { + return {} +} +function FromArray(schema: TArray, references: TSchema[]): any { + if (schema.uniqueItems === true && !HasPropertyKey(schema, 'default')) { + throw new ValueCreateError(schema, 'Array with the uniqueItems constraint requires a default value') + } else if ('contains' in schema && !HasPropertyKey(schema, 'default')) { + throw new ValueCreateError(schema, 'Array with the contains constraint requires a default value') + } else if ('default' in schema) { + return FromDefault(schema.default) + } else if (schema.minItems !== undefined) { + return Array.from({ length: schema.minItems }).map((item) => { + return Visit(schema.items, references) + }) + } else { + return [] + } +} +function FromAsyncIterator(schema: TAsyncIterator, references: TSchema[]) { + if (HasPropertyKey(schema, 'default')) { + return FromDefault(schema.default) + } else { + return (async function* () {})() + } +} +function FromBigInt(schema: TBigInt, references: TSchema[]): any { + if (HasPropertyKey(schema, 'default')) { + return FromDefault(schema.default) + } else { + return BigInt(0) + } +} +function FromBoolean(schema: TBoolean, references: TSchema[]): any { + if (HasPropertyKey(schema, 'default')) { + return FromDefault(schema.default) + } else { + return false + } +} +function FromConstructor(schema: TConstructor, references: TSchema[]): any { + if (HasPropertyKey(schema, 'default')) { + return FromDefault(schema.default) + } else { + const value = Visit(schema.returns, references) as any + if (typeof value === 'object' && !Array.isArray(value)) { + return class { + constructor() { + for (const [key, val] of Object.entries(value)) { + const self = this as any + self[key] = val + } + } + } + } else { + return class {} + } + } +} +function FromDate(schema: TDate, references: TSchema[]): any { + if (HasPropertyKey(schema, 'default')) { + return FromDefault(schema.default) + } else if (schema.minimumTimestamp !== undefined) { + return new Date(schema.minimumTimestamp) + } else { + return new Date() + } +} +function FromFunction(schema: TFunction, references: TSchema[]): any { + if (HasPropertyKey(schema, 'default')) { + return FromDefault(schema.default) + } else { + return () => Visit(schema.returns, references) + } +} +function FromImport(schema: TImport, references: TSchema[]): any { + const definitions = globalThis.Object.values(schema.$defs) as TSchema[] + const target = schema.$defs[schema.$ref] as TSchema + return Visit(target, [...references, ...definitions]) +} +function FromInteger(schema: TInteger, references: TSchema[]): any { + if (HasPropertyKey(schema, 'default')) { + return FromDefault(schema.default) + } else if (schema.minimum !== undefined) { + return schema.minimum + } else { + return 0 + } +} +function FromIntersect(schema: TIntersect, references: TSchema[]): any { + if (HasPropertyKey(schema, 'default')) { + return FromDefault(schema.default) + } else { + // -------------------------------------------------------------- + // Note: The best we can do here is attempt to instance each + // sub type and apply through object assign. For non-object + // sub types, we just escape the assignment and just return + // the value. In the latter case, this is typically going to + // be a consequence of an illogical intersection. + // -------------------------------------------------------------- + const value = schema.allOf.reduce((acc, schema) => { + const next = Visit(schema, references) as any + return typeof next === 'object' ? { ...acc, ...next } : next + }, {}) + if (!Check(schema, references, value)) throw new ValueCreateError(schema, 'Intersect produced invalid value. Consider using a default value.') + return value + } +} +function FromIterator(schema: TIterator, references: TSchema[]) { + if (HasPropertyKey(schema, 'default')) { + return FromDefault(schema.default) + } else { + return (function* () {})() + } +} +function FromLiteral(schema: TLiteral, references: TSchema[]): any { + if (HasPropertyKey(schema, 'default')) { + return FromDefault(schema.default) + } else { + return schema.const + } +} +function FromNever(schema: TNever, references: TSchema[]): any { + if (HasPropertyKey(schema, 'default')) { + return FromDefault(schema.default) + } else { + throw new ValueCreateError(schema, 'Never types cannot be created. Consider using a default value.') + } +} +function FromNot(schema: TNot, references: TSchema[]): any { + if (HasPropertyKey(schema, 'default')) { + return FromDefault(schema.default) + } else { + throw new ValueCreateError(schema, 'Not types must have a default value') + } +} +function FromNull(schema: TNull, references: TSchema[]): any { + if (HasPropertyKey(schema, 'default')) { + return FromDefault(schema.default) + } else { + return null + } +} +function FromNumber(schema: TNumber, references: TSchema[]): any { + if (HasPropertyKey(schema, 'default')) { + return FromDefault(schema.default) + } else if (schema.minimum !== undefined) { + return schema.minimum + } else { + return 0 + } +} +function FromObject(schema: TObject, references: TSchema[]): any { + if (HasPropertyKey(schema, 'default')) { + return FromDefault(schema.default) + } else { + const required = new Set(schema.required) + const Acc = {} as Record + for (const [key, subschema] of Object.entries(schema.properties)) { + if (!required.has(key)) continue + Acc[key] = Visit(subschema, references) + } + return Acc + } +} +function FromPromise(schema: TPromise, references: TSchema[]): any { + if (HasPropertyKey(schema, 'default')) { + return FromDefault(schema.default) + } else { + return Promise.resolve(Visit(schema.item, references)) + } +} +function FromRecord(schema: TRecord, references: TSchema[]): any { + if (HasPropertyKey(schema, 'default')) { + return FromDefault(schema.default) + } else { + return {} + } +} +function FromRef(schema: TRef, references: TSchema[]): any { + if (HasPropertyKey(schema, 'default')) { + return FromDefault(schema.default) + } else { + return Visit(Deref(schema, references), references) + } +} +function FromRegExp(schema: TRegExp, references: TSchema[]): any { + if (HasPropertyKey(schema, 'default')) { + return FromDefault(schema.default) + } else { + throw new ValueCreateError(schema, 'RegExp types cannot be created. Consider using a default value.') + } +} +function FromString(schema: TString, references: TSchema[]): any { + if (schema.pattern !== undefined) { + if (!HasPropertyKey(schema, 'default')) { + throw new ValueCreateError(schema, 'String types with patterns must specify a default value') + } else { + return FromDefault(schema.default) + } + } else if (schema.format !== undefined) { + if (!HasPropertyKey(schema, 'default')) { + throw new ValueCreateError(schema, 'String types with formats must specify a default value') + } else { + return FromDefault(schema.default) + } + } else { + if (HasPropertyKey(schema, 'default')) { + return FromDefault(schema.default) + } else if (schema.minLength !== undefined) { + // prettier-ignore + return Array.from({ length: schema.minLength }).map(() => ' ').join('') + } else { + return '' + } + } +} +function FromSymbol(schema: TSymbol, references: TSchema[]): any { + if (HasPropertyKey(schema, 'default')) { + return FromDefault(schema.default) + } else if ('value' in schema) { + return Symbol.for(schema.value) + } else { + return Symbol() + } +} +function FromTemplateLiteral(schema: TTemplateLiteral, references: TSchema[]) { + if (HasPropertyKey(schema, 'default')) { + return FromDefault(schema.default) + } + if (!IsTemplateLiteralFinite(schema)) throw new ValueCreateError(schema, 'Can only create template literals that produce a finite variants. Consider using a default value.') + const generated = TemplateLiteralGenerate(schema) as string[] + return generated[0] +} +function FromThis(schema: TThis, references: TSchema[]): any { + if (recursiveDepth++ > recursiveMaxDepth) throw new ValueCreateError(schema, 'Cannot create recursive type as it appears possibly infinite. Consider using a default.') + if (HasPropertyKey(schema, 'default')) { + return FromDefault(schema.default) + } else { + return Visit(Deref(schema, references), references) + } +} +function FromTuple(schema: TTuple, references: TSchema[]): any { + if (HasPropertyKey(schema, 'default')) { + return FromDefault(schema.default) + } + if (schema.items === undefined) { + return [] + } else { + return Array.from({ length: schema.minItems }).map((_, index) => Visit((schema.items as any[])[index], references)) + } +} +function FromUndefined(schema: TUndefined, references: TSchema[]): any { + if (HasPropertyKey(schema, 'default')) { + return FromDefault(schema.default) + } else { + return undefined + } +} +function FromUnion(schema: TUnion, references: TSchema[]): any { + if (HasPropertyKey(schema, 'default')) { + return FromDefault(schema.default) + } else if (schema.anyOf.length === 0) { + throw new Error('ValueCreate.Union: Cannot create Union with zero variants') + } else { + return Visit(schema.anyOf[0], references) + } +} +function FromUint8Array(schema: TUint8Array, references: TSchema[]): any { + if (HasPropertyKey(schema, 'default')) { + return FromDefault(schema.default) + } else if (schema.minByteLength !== undefined) { + return new Uint8Array(schema.minByteLength) + } else { + return new Uint8Array(0) + } +} +function FromUnknown(schema: TUnknown, references: TSchema[]): any { + if (HasPropertyKey(schema, 'default')) { + return FromDefault(schema.default) + } else { + return {} + } +} +function FromVoid(schema: TVoid, references: TSchema[]): any { + if (HasPropertyKey(schema, 'default')) { + return FromDefault(schema.default) + } else { + return void 0 + } +} +function FromKind(schema: TSchema, references: TSchema[]): any { + if (HasPropertyKey(schema, 'default')) { + return FromDefault(schema.default) + } else { + throw new Error('User defined types must specify a default value') + } +} +function Visit(schema: TSchema, references: TSchema[]): unknown { + const references_ = Pushref(schema, references) + const schema_ = schema as any + switch (schema_[Kind]) { + case 'Any': + return FromAny(schema_, references_) + case 'Argument': + return FromArgument(schema_, references_) + case 'Array': + return FromArray(schema_, references_) + case 'AsyncIterator': + return FromAsyncIterator(schema_, references_) + case 'BigInt': + return FromBigInt(schema_, references_) + case 'Boolean': + return FromBoolean(schema_, references_) + case 'Constructor': + return FromConstructor(schema_, references_) + case 'Date': + return FromDate(schema_, references_) + case 'Function': + return FromFunction(schema_, references_) + case 'Import': + return FromImport(schema_, references_) + case 'Integer': + return FromInteger(schema_, references_) + case 'Intersect': + return FromIntersect(schema_, references_) + case 'Iterator': + return FromIterator(schema_, references_) + case 'Literal': + return FromLiteral(schema_, references_) + case 'Never': + return FromNever(schema_, references_) + case 'Not': + return FromNot(schema_, references_) + case 'Null': + return FromNull(schema_, references_) + case 'Number': + return FromNumber(schema_, references_) + case 'Object': + return FromObject(schema_, references_) + case 'Promise': + return FromPromise(schema_, references_) + case 'Record': + return FromRecord(schema_, references_) + case 'Ref': + return FromRef(schema_, references_) + case 'RegExp': + return FromRegExp(schema_, references_) + case 'String': + return FromString(schema_, references_) + case 'Symbol': + return FromSymbol(schema_, references_) + case 'TemplateLiteral': + return FromTemplateLiteral(schema_, references_) + case 'This': + return FromThis(schema_, references_) + case 'Tuple': + return FromTuple(schema_, references_) + case 'Undefined': + return FromUndefined(schema_, references_) + case 'Union': + return FromUnion(schema_, references_) + case 'Uint8Array': + return FromUint8Array(schema_, references_) + case 'Unknown': + return FromUnknown(schema_, references_) + case 'Void': + return FromVoid(schema_, references_) + default: + if (!TypeRegistry.Has(schema_[Kind])) throw new ValueCreateError(schema_, 'Unknown type') + return FromKind(schema_, references_) + } +} +// ------------------------------------------------------------------ +// State +// ------------------------------------------------------------------ +const recursiveMaxDepth = 512 +let recursiveDepth = 0 +// ------------------------------------------------------------------ +// Create +// ------------------------------------------------------------------ +/** Creates a value from the given schema and references */ +export function Create(schema: T, references: TSchema[]): Static +/** Creates a value from the given schema */ +export function Create(schema: T): Static +/** Creates a value from the given schema */ +export function Create(...args: any[]) { + recursiveDepth = 0 + return args.length === 2 ? Visit(args[0], args[1]) : Visit(args[0], []) +} diff --git a/src/value/create/index.ts b/src/value/create/index.ts new file mode 100644 index 000000000..adeb52ec6 --- /dev/null +++ b/src/value/create/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/value + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './create' diff --git a/src/value/decode/decode.ts b/src/value/decode/decode.ts new file mode 100644 index 000000000..0ff9a238a --- /dev/null +++ b/src/value/decode/decode.ts @@ -0,0 +1,45 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/value + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { HasTransform, TransformDecode, TransformDecodeCheckError } from '../transform/index' +import { Check } from '../check/index' +import { Errors } from '../../errors/index' + +import type { TSchema } from '../../type/schema/index' +import type { StaticDecode } from '../../type/static/index' + +/** Decodes a value or throws if error */ +export function Decode, Result extends Static = Static>(schema: T, references: TSchema[], value: unknown): Result +/** Decodes a value or throws if error */ +export function Decode, Result extends Static = Static>(schema: T, value: unknown): Result +/** Decodes a value or throws if error */ +export function Decode(...args: any[]): any { + const [schema, references, value] = args.length === 3 ? [args[0], args[1], args[2]] : [args[0], [], args[1]] + if (!Check(schema, references, value)) throw new TransformDecodeCheckError(schema, value, Errors(schema, references, value).First()!) + return HasTransform(schema, references) ? TransformDecode(schema, references, value) : value +} diff --git a/src/value/decode/index.ts b/src/value/decode/index.ts new file mode 100644 index 000000000..6c82cdbe7 --- /dev/null +++ b/src/value/decode/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/value + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './decode' diff --git a/src/value/default/default.ts b/src/value/default/default.ts new file mode 100644 index 000000000..5737c8d91 --- /dev/null +++ b/src/value/default/default.ts @@ -0,0 +1,208 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/value + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { Check } from '../check/index' +import { Clone } from '../clone/index' +import { Deref, Pushref } from '../deref/index' +import { Kind } from '../../type/symbols/index' + +import type { TSchema } from '../../type/schema/index' +import type { TArray } from '../../type/array/index' +import type { TImport } from '../../type/module/index' +import type { TIntersect } from '../../type/intersect/index' +import type { TObject } from '../../type/object/index' +import type { TRecord } from '../../type/record/index' +import type { TRef } from '../../type/ref/index' +import type { TThis } from '../../type/recursive/index' +import type { TTuple } from '../../type/tuple/index' +import type { TUnion } from '../../type/union/index' + +// ------------------------------------------------------------------ +// ValueGuard +// ------------------------------------------------------------------ +import { IsArray, IsDate, IsFunction, IsObject, IsUndefined, HasPropertyKey } from '../guard/index' +// ------------------------------------------------------------------ +// TypeGuard +// ------------------------------------------------------------------ +import { IsKind } from '../../type/guard/kind' +// ------------------------------------------------------------------ +// ValueOrDefault +// ------------------------------------------------------------------ +function ValueOrDefault(schema: TSchema, value: unknown): unknown { + const defaultValue = HasPropertyKey(schema, 'default') ? schema.default : undefined + const clone = IsFunction(defaultValue) ? defaultValue() : Clone(defaultValue) + return IsUndefined(value) ? clone : IsObject(value) && IsObject(clone) ? Object.assign(clone, value) : value +} +// ------------------------------------------------------------------ +// HasDefaultProperty +// ------------------------------------------------------------------ +function HasDefaultProperty(schema: unknown): schema is TSchema { + return IsKind(schema) && 'default' in schema +} +// ------------------------------------------------------------------ +// Types +// ------------------------------------------------------------------ +function FromArray(schema: TArray, references: TSchema[], value: unknown): any { + // if the value is an array, we attempt to initialize it's elements + if (IsArray(value)) { + for (let i = 0; i < value.length; i++) { + value[i] = Visit(schema.items, references, value[i]) + } + return value + } + // ... otherwise use default initialization + const defaulted = ValueOrDefault(schema, value) + if (!IsArray(defaulted)) return defaulted + for (let i = 0; i < defaulted.length; i++) { + defaulted[i] = Visit(schema.items, references, defaulted[i]) + } + return defaulted +} +function FromDate(schema: TArray, references: TSchema[], value: unknown): any { + // special case intercept for dates + return IsDate(value) ? value : ValueOrDefault(schema, value) +} +function FromImport(schema: TImport, references: TSchema[], value: unknown): any { + const definitions = globalThis.Object.values(schema.$defs) as TSchema[] + const target = schema.$defs[schema.$ref] as TSchema + return Visit(target, [...references, ...definitions], value) +} +function FromIntersect(schema: TIntersect, references: TSchema[], value: unknown): any { + const defaulted = ValueOrDefault(schema, value) + return schema.allOf.reduce((acc, schema) => { + const next = Visit(schema, references, defaulted) + return IsObject(next) ? { ...acc, ...next } : next + }, {}) +} +function FromObject(schema: TObject, references: TSchema[], value: unknown): any { + const defaulted = ValueOrDefault(schema, value) + // return defaulted + if (!IsObject(defaulted)) return defaulted + const knownPropertyKeys = Object.getOwnPropertyNames(schema.properties) + // properties + for (const key of knownPropertyKeys) { + // note: we need to traverse into the object and test if the return value + // yielded a non undefined result. Here we interpret an undefined result as + // a non assignable property and continue. + const propertyValue = Visit(schema.properties[key], references, defaulted[key]) + if (IsUndefined(propertyValue)) continue + defaulted[key] = Visit(schema.properties[key], references, defaulted[key]) + } + // return if not additional properties + if (!HasDefaultProperty(schema.additionalProperties)) return defaulted + // additional properties + for (const key of Object.getOwnPropertyNames(defaulted)) { + if (knownPropertyKeys.includes(key)) continue + defaulted[key] = Visit(schema.additionalProperties, references, defaulted[key]) + } + return defaulted +} +function FromRecord(schema: TRecord, references: TSchema[], value: unknown): any { + const defaulted = ValueOrDefault(schema, value) + if (!IsObject(defaulted)) return defaulted + const additionalPropertiesSchema = schema.additionalProperties as TSchema + const [propertyKeyPattern, propertySchema] = Object.entries(schema.patternProperties)[0] + const knownPropertyKey = new RegExp(propertyKeyPattern) + // properties + for (const key of Object.getOwnPropertyNames(defaulted)) { + if (!(knownPropertyKey.test(key) && HasDefaultProperty(propertySchema))) continue + defaulted[key] = Visit(propertySchema, references, defaulted[key]) + } + // return if not additional properties + if (!HasDefaultProperty(additionalPropertiesSchema)) return defaulted + // additional properties + for (const key of Object.getOwnPropertyNames(defaulted)) { + if (knownPropertyKey.test(key)) continue + defaulted[key] = Visit(additionalPropertiesSchema, references, defaulted[key]) + } + return defaulted +} +function FromRef(schema: TRef, references: TSchema[], value: unknown): any { + return Visit(Deref(schema, references), references, ValueOrDefault(schema, value)) +} +function FromThis(schema: TThis, references: TSchema[], value: unknown): any { + return Visit(Deref(schema, references), references, value) +} +function FromTuple(schema: TTuple, references: TSchema[], value: unknown): any { + const defaulted = ValueOrDefault(schema, value) + if (!IsArray(defaulted) || IsUndefined(schema.items)) return defaulted + const [items, max] = [schema.items!, Math.max(schema.items!.length, defaulted.length)] + for (let i = 0; i < max; i++) { + if (i < items.length) defaulted[i] = Visit(items[i], references, defaulted[i]) + } + return defaulted +} +function FromUnion(schema: TUnion, references: TSchema[], value: unknown): any { + const defaulted = ValueOrDefault(schema, value) + for (const inner of schema.anyOf) { + const result = Visit(inner, references, Clone(defaulted)) + if (Check(inner, references, result)) { + return result + } + } + return defaulted +} +function Visit(schema: TSchema, references: TSchema[], value: unknown): any { + const references_ = Pushref(schema, references) + const schema_ = schema as any + switch (schema_[Kind]) { + case 'Array': + return FromArray(schema_, references_, value) + case 'Date': + return FromDate(schema_, references_, value) + case 'Import': + return FromImport(schema_, references_, value) + case 'Intersect': + return FromIntersect(schema_, references_, value) + case 'Object': + return FromObject(schema_, references_, value) + case 'Record': + return FromRecord(schema_, references_, value) + case 'Ref': + return FromRef(schema_, references_, value) + case 'This': + return FromThis(schema_, references_, value) + case 'Tuple': + return FromTuple(schema_, references_, value) + case 'Union': + return FromUnion(schema_, references_, value) + default: + return ValueOrDefault(schema_, value) + } +} +// ------------------------------------------------------------------ +// Default +// ------------------------------------------------------------------ +/** `[Mutable]` Generates missing properties on a value using default schema annotations if available. This function does not check the value and returns an unknown type. You should Check the result before use. Default is a mutable operation. To avoid mutation, Clone the value first. */ +export function Default(schema: TSchema, references: TSchema[], value: unknown): unknown +/** `[Mutable]` Generates missing properties on a value using default schema annotations if available. This function does not check the value and returns an unknown type. You should Check the result before use. Default is a mutable operation. To avoid mutation, Clone the value first. */ +export function Default(schema: TSchema, value: unknown): unknown +/** `[Mutable]` Generates missing properties on a value using default schema annotations if available. This function does not check the value and returns an unknown type. You should Check the result before use. Default is a mutable operation. To avoid mutation, Clone the value first. */ +export function Default(...args: any[]) { + return args.length === 3 ? Visit(args[0], args[1], args[2]) : Visit(args[0], [], args[1]) +} diff --git a/src/value/default/index.ts b/src/value/default/index.ts new file mode 100644 index 000000000..9619cd8c5 --- /dev/null +++ b/src/value/default/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/value + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './default' diff --git a/src/value/delta/delta.ts b/src/value/delta/delta.ts new file mode 100644 index 000000000..dc9d41034 --- /dev/null +++ b/src/value/delta/delta.ts @@ -0,0 +1,215 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/value + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { HasPropertyKey, IsStandardObject, IsArray, IsTypedArray, IsValueType } from '../guard/index' +import type { ObjectType, ArrayType, TypedArrayType, ValueType } from '../guard/index' +import type { Static } from '../../type/static/index' +import { ValuePointer } from '../pointer/index' +import { Clone } from '../clone/index' +import { Equal } from '../equal/equal' +import { TypeBoxError } from '../../type/error/index' + +import { Literal, type TLiteral } from '../../type/literal/index' +import { Object, type TObject } from '../../type/object/index' +import { String, type TString } from '../../type/string/index' +import { Unknown, type TUnknown } from '../../type/unknown/index' +import { Union, type TUnion } from '../../type/union/index' + +// ------------------------------------------------------------------ +// Commands +// ------------------------------------------------------------------ + +// Note: A TypeScript 5.4.2 compiler regression resulted in the type +// import paths being generated incorrectly. We can resolve this by +// explicitly importing the correct TSchema types above. Note also +// that the left-side annotations are optional, but since the types +// are imported we might as well use them. We should check this +// regression in future. The regression occured between TypeScript +// versions 5.3.3 -> 5.4.2. + +export type Insert = Static +export const Insert: TObject<{ + type: TLiteral<'insert'> + path: TString + value: TUnknown +}> = Object({ + type: Literal('insert'), + path: String(), + value: Unknown(), +}) +export type Update = Static +export const Update: TObject<{ + type: TLiteral<'update'> + path: TString + value: TUnknown +}> = Object({ + type: Literal('update'), + path: String(), + value: Unknown(), +}) +export type Delete = Static +export const Delete: TObject<{ + type: TLiteral<'delete'> + path: TString +}> = Object({ + type: Literal('delete'), + path: String(), +}) +export type Edit = Static +export const Edit: TUnion<[typeof Insert, typeof Update, typeof Delete]> = Union([Insert, Update, Delete]) + +// ------------------------------------------------------------------ +// Errors +// ------------------------------------------------------------------ +export class ValueDiffError extends TypeBoxError { + constructor(public readonly value: unknown, message: string) { + super(message) + } +} +// ------------------------------------------------------------------ +// Command Factory +// ------------------------------------------------------------------ +function CreateUpdate(path: string, value: unknown): Edit { + return { type: 'update', path, value } +} +function CreateInsert(path: string, value: unknown): Edit { + return { type: 'insert', path, value } +} +function CreateDelete(path: string): Edit { + return { type: 'delete', path } +} +// ------------------------------------------------------------------ +// AssertDiffable +// ------------------------------------------------------------------ +function AssertDiffable(value: unknown): asserts value is Record { + if (globalThis.Object.getOwnPropertySymbols(value).length > 0) throw new ValueDiffError(value, 'Cannot diff objects with symbols') +} +// ------------------------------------------------------------------ +// Diffing Generators +// ------------------------------------------------------------------ +function* ObjectType(path: string, current: ObjectType, next: unknown): IterableIterator { + AssertDiffable(current) + AssertDiffable(next) + if (!IsStandardObject(next)) return yield CreateUpdate(path, next) + const currentKeys = globalThis.Object.getOwnPropertyNames(current) + const nextKeys = globalThis.Object.getOwnPropertyNames(next) + // ---------------------------------------------------------------- + // inserts + // ---------------------------------------------------------------- + for (const key of nextKeys) { + if (HasPropertyKey(current, key)) continue + yield CreateInsert(`${path}/${key}`, next[key]) + } + // ---------------------------------------------------------------- + // updates + // ---------------------------------------------------------------- + for (const key of currentKeys) { + if (!HasPropertyKey(next, key)) continue + if (Equal(current, next)) continue + yield* Visit(`${path}/${key}`, current[key], next[key]) + } + // ---------------------------------------------------------------- + // deletes + // ---------------------------------------------------------------- + for (const key of currentKeys) { + if (HasPropertyKey(next, key)) continue + yield CreateDelete(`${path}/${key}`) + } +} +function* ArrayType(path: string, current: ArrayType, next: unknown): IterableIterator { + if (!IsArray(next)) return yield CreateUpdate(path, next) + for (let i = 0; i < Math.min(current.length, next.length); i++) { + yield* Visit(`${path}/${i}`, current[i], next[i]) + } + for (let i = 0; i < next.length; i++) { + if (i < current.length) continue + yield CreateInsert(`${path}/${i}`, next[i]) + } + for (let i = current.length - 1; i >= 0; i--) { + if (i < next.length) continue + yield CreateDelete(`${path}/${i}`) + } +} +function* TypedArrayType(path: string, current: TypedArrayType, next: unknown): IterableIterator { + if (!IsTypedArray(next) || current.length !== next.length || globalThis.Object.getPrototypeOf(current).constructor.name !== globalThis.Object.getPrototypeOf(next).constructor.name) return yield CreateUpdate(path, next) + for (let i = 0; i < Math.min(current.length, next.length); i++) { + yield* Visit(`${path}/${i}`, current[i], next[i]) + } +} +function* ValueType(path: string, current: ValueType, next: unknown): IterableIterator { + if (current === next) return + yield CreateUpdate(path, next) +} +function* Visit(path: string, current: unknown, next: unknown): IterableIterator { + if (IsStandardObject(current)) return yield* ObjectType(path, current, next) + if (IsArray(current)) return yield* ArrayType(path, current, next) + if (IsTypedArray(current)) return yield* TypedArrayType(path, current, next) + if (IsValueType(current)) return yield* ValueType(path, current, next) + throw new ValueDiffError(current, 'Unable to diff value') +} +// ------------------------------------------------------------------ +// Diff +// ------------------------------------------------------------------ +export function Diff(current: unknown, next: unknown): Edit[] { + return [...Visit('', current, next)] +} +// ------------------------------------------------------------------ +// Patch +// ------------------------------------------------------------------ +function IsRootUpdate(edits: Edit[]): edits is [Update] { + return edits.length > 0 && edits[0].path === '' && edits[0].type === 'update' +} +function IsIdentity(edits: Edit[]) { + return edits.length === 0 +} +export function Patch(current: unknown, edits: Edit[]): T { + if (IsRootUpdate(edits)) { + return Clone(edits[0].value) as T + } + if (IsIdentity(edits)) { + return Clone(current) as T + } + const clone = Clone(current) + for (const edit of edits) { + switch (edit.type) { + case 'insert': { + ValuePointer.Set(clone, edit.path, edit.value) + break + } + case 'update': { + ValuePointer.Set(clone, edit.path, edit.value) + break + } + case 'delete': { + ValuePointer.Delete(clone, edit.path) + break + } + } + } + return clone as T +} diff --git a/src/value/delta/index.ts b/src/value/delta/index.ts new file mode 100644 index 000000000..64b7dd1d7 --- /dev/null +++ b/src/value/delta/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/value + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './delta' diff --git a/src/value/deref/deref.ts b/src/value/deref/deref.ts new file mode 100644 index 000000000..df2d80881 --- /dev/null +++ b/src/value/deref/deref.ts @@ -0,0 +1,59 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/value + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import type { TSchema } from '../../type/schema/index' +import type { TRef } from '../../type/ref/index' +import type { TThis } from '../../type/recursive/index' +import { TypeBoxError } from '../../type/error/index' +import { Kind } from '../../type/symbols/index' +import { IsString } from '../guard/guard' +export class TypeDereferenceError extends TypeBoxError { + constructor(public readonly schema: TRef | TThis) { + super(`Unable to dereference schema with $id '${schema.$ref}'`) + } +} +function Resolve(schema: TThis | TRef, references: TSchema[]): TSchema { + const target = references.find((target) => target.$id === schema.$ref) + if (target === undefined) throw new TypeDereferenceError(schema) + return Deref(target, references) +} + +/** `[Internal]` Pushes a schema onto references if the schema has an $id and does not exist on references */ +export function Pushref(schema: TSchema, references: TSchema[]): TSchema[] { + if (!IsString(schema.$id) || references.some((target) => target.$id === schema.$id)) return references + references.push(schema) + return references +} + +/** `[Internal]` Dereferences a schema from the references array or throws if not found */ +export function Deref(schema: TSchema, references: TSchema[]): TSchema { + // prettier-ignore + return (schema[Kind] === 'This' || schema[Kind] === 'Ref') + ? Resolve(schema as never, references) + : schema +} diff --git a/src/value/deref/index.ts b/src/value/deref/index.ts new file mode 100644 index 000000000..0603b9430 --- /dev/null +++ b/src/value/deref/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/value + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './deref' diff --git a/src/value/encode/encode.ts b/src/value/encode/encode.ts new file mode 100644 index 000000000..be52b4a62 --- /dev/null +++ b/src/value/encode/encode.ts @@ -0,0 +1,46 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/value + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { HasTransform, TransformEncode, TransformEncodeCheckError } from '../transform/index' +import { Check } from '../check/index' +import { Errors } from '../../errors/index' + +import type { TSchema } from '../../type/schema/index' +import type { StaticEncode } from '../../type/static/index' + +/** Encodes a value or throws if error */ +export function Encode, Result extends Static = Static>(schema: T, references: TSchema[], value: unknown): Result +/** Encodes a value or throws if error */ +export function Encode, Result extends Static = Static>(schema: T, value: unknown): Result +/** Encodes a value or throws if error */ +export function Encode(...args: any[]): any { + const [schema, references, value] = args.length === 3 ? [args[0], args[1], args[2]] : [args[0], [], args[1]] + const encoded = HasTransform(schema, references) ? TransformEncode(schema, references, value) : value + if (!Check(schema, references, encoded)) throw new TransformEncodeCheckError(schema, encoded, Errors(schema, references, encoded).First()!) + return encoded +} diff --git a/src/value/encode/index.ts b/src/value/encode/index.ts new file mode 100644 index 000000000..e4eb54b1c --- /dev/null +++ b/src/value/encode/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/value + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './encode' diff --git a/src/value/equal/equal.ts b/src/value/equal/equal.ts new file mode 100644 index 000000000..2cceefd82 --- /dev/null +++ b/src/value/equal/equal.ts @@ -0,0 +1,67 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/value + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { IsObject, IsDate, IsArray, IsTypedArray, IsValueType } from '../guard/index' +import type { ObjectType, ArrayType, TypedArrayType, ValueType } from '../guard/index' + +// ------------------------------------------------------------------ +// Equality Checks +// ------------------------------------------------------------------ +function ObjectType(left: ObjectType, right: unknown): boolean { + if (!IsObject(right)) return false + const leftKeys = [...Object.keys(left), ...Object.getOwnPropertySymbols(left)] + const rightKeys = [...Object.keys(right), ...Object.getOwnPropertySymbols(right)] + if (leftKeys.length !== rightKeys.length) return false + return leftKeys.every((key) => Equal(left[key], right[key])) +} +function DateType(left: Date, right: unknown): any { + return IsDate(right) && left.getTime() === right.getTime() +} +function ArrayType(left: ArrayType, right: unknown): any { + if (!IsArray(right) || left.length !== right.length) return false + return left.every((value, index) => Equal(value, right[index])) +} +function TypedArrayType(left: TypedArrayType, right: unknown): any { + if (!IsTypedArray(right) || left.length !== right.length || Object.getPrototypeOf(left).constructor.name !== Object.getPrototypeOf(right).constructor.name) return false + return left.every((value, index) => Equal(value, right[index])) +} +function ValueType(left: ValueType, right: unknown): any { + return left === right +} +// ------------------------------------------------------------------ +// Equal +// ------------------------------------------------------------------ +/** Returns true if the left value deep-equals the right */ +export function Equal(left: T, right: unknown): right is T { + if (IsDate(left)) return DateType(left, right) + if (IsTypedArray(left)) return TypedArrayType(left, right) + if (IsArray(left)) return ArrayType(left, right) + if (IsObject(left)) return ObjectType(left, right) + if (IsValueType(left)) return ValueType(left, right) + throw new Error('ValueEquals: Unable to compare value') +} diff --git a/src/value/equal/index.ts b/src/value/equal/index.ts new file mode 100644 index 000000000..17c146250 --- /dev/null +++ b/src/value/equal/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/value + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './equal' diff --git a/src/value/guard/guard.ts b/src/value/guard/guard.ts new file mode 100644 index 000000000..56410e709 --- /dev/null +++ b/src/value/guard/guard.ts @@ -0,0 +1,207 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/value + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +// ------------------------------------------------------------------ +// Types +// ------------------------------------------------------------------ +export type ObjectType = Record +export type ArrayType = unknown[] +export type ValueType = null | undefined | symbol | bigint | number | boolean | string +// prettier-ignore +export type TypedArrayType = + | Int8Array + | Uint8Array + | Uint8ClampedArray + | Int16Array + | Uint16Array + | Int32Array + | Uint32Array + | Float32Array + | Float64Array + | BigInt64Array + | BigUint64Array +// -------------------------------------------------------------------------- +// Iterators +// -------------------------------------------------------------------------- +/** Returns true if this value is an async iterator */ +export function IsAsyncIterator(value: unknown): value is AsyncIterableIterator { + return IsObject(value) && globalThis.Symbol.asyncIterator in value +} +/** Returns true if this value is an iterator */ +export function IsIterator(value: unknown): value is IterableIterator { + return IsObject(value) && globalThis.Symbol.iterator in value +} +// -------------------------------------------------------------------------- +// Object Instances +// -------------------------------------------------------------------------- +/** Returns true if this value is not an instance of a class */ +export function IsStandardObject(value: unknown): value is ObjectType { + return IsObject(value) && (globalThis.Object.getPrototypeOf(value) === Object.prototype || globalThis.Object.getPrototypeOf(value) === null) +} +/** Returns true if this value is an instance of a class */ +export function IsInstanceObject(value: unknown): value is ObjectType { + return IsObject(value) && !IsArray(value) && IsFunction(value.constructor) && value.constructor.name !== 'Object' +} +// -------------------------------------------------------------------------- +// JavaScript +// -------------------------------------------------------------------------- +/** Returns true if this value is a Promise */ +export function IsPromise(value: unknown): value is Promise { + return value instanceof globalThis.Promise +} +/** Returns true if this value is a Date */ +export function IsDate(value: unknown): value is Date { + return value instanceof Date && globalThis.Number.isFinite(value.getTime()) +} +/** Returns true if this value is an instance of Map */ +export function IsMap(value: unknown): value is Map { + return value instanceof globalThis.Map +} +/** Returns true if this value is an instance of Set */ +export function IsSet(value: unknown): value is Set { + return value instanceof globalThis.Set +} +/** Returns true if this value is RegExp */ +export function IsRegExp(value: unknown): value is RegExp { + return value instanceof globalThis.RegExp +} +/** Returns true if this value is a typed array */ +export function IsTypedArray(value: unknown): value is TypedArrayType { + return globalThis.ArrayBuffer.isView(value) +} +/** Returns true if the value is a Int8Array */ +export function IsInt8Array(value: unknown): value is Int8Array { + return value instanceof globalThis.Int8Array +} +/** Returns true if the value is a Uint8Array */ +export function IsUint8Array(value: unknown): value is Uint8Array { + return value instanceof globalThis.Uint8Array +} +/** Returns true if the value is a Uint8ClampedArray */ +export function IsUint8ClampedArray(value: unknown): value is Uint8ClampedArray { + return value instanceof globalThis.Uint8ClampedArray +} +/** Returns true if the value is a Int16Array */ +export function IsInt16Array(value: unknown): value is Int16Array { + return value instanceof globalThis.Int16Array +} +/** Returns true if the value is a Uint16Array */ +export function IsUint16Array(value: unknown): value is Uint16Array { + return value instanceof globalThis.Uint16Array +} +/** Returns true if the value is a Int32Array */ +export function IsInt32Array(value: unknown): value is Int32Array { + return value instanceof globalThis.Int32Array +} +/** Returns true if the value is a Uint32Array */ +export function IsUint32Array(value: unknown): value is Uint32Array { + return value instanceof globalThis.Uint32Array +} +/** Returns true if the value is a Float32Array */ +export function IsFloat32Array(value: unknown): value is Float32Array { + return value instanceof globalThis.Float32Array +} +/** Returns true if the value is a Float64Array */ +export function IsFloat64Array(value: unknown): value is Float64Array { + return value instanceof globalThis.Float64Array +} +/** Returns true if the value is a BigInt64Array */ +export function IsBigInt64Array(value: unknown): value is BigInt64Array { + return value instanceof globalThis.BigInt64Array +} +/** Returns true if the value is a BigUint64Array */ +export function IsBigUint64Array(value: unknown): value is BigUint64Array { + return value instanceof globalThis.BigUint64Array +} +// -------------------------------------------------------------------------- +// PropertyKey +// -------------------------------------------------------------------------- +/** Returns true if this value has this property key */ +export function HasPropertyKey(value: Record, key: K): value is Record & { [_ in K]: unknown } { + return key in value +} +// -------------------------------------------------------------------------- +// Standard +// -------------------------------------------------------------------------- +/** Returns true of this value is an object type */ +export function IsObject(value: unknown): value is ObjectType { + return value !== null && typeof value === 'object' +} +/** Returns true if this value is an array, but not a typed array */ +export function IsArray(value: unknown): value is ArrayType { + return globalThis.Array.isArray(value) && !globalThis.ArrayBuffer.isView(value) +} +/** Returns true if this value is an undefined */ +export function IsUndefined(value: unknown): value is undefined { + return value === undefined +} +/** Returns true if this value is an null */ +export function IsNull(value: unknown): value is null { + return value === null +} +/** Returns true if this value is an boolean */ +export function IsBoolean(value: unknown): value is boolean { + return typeof value === 'boolean' +} +/** Returns true if this value is an number */ +export function IsNumber(value: unknown): value is number { + return typeof value === 'number' +} +/** Returns true if this value is an integer */ +export function IsInteger(value: unknown): value is number { + return globalThis.Number.isInteger(value) +} +/** Returns true if this value is bigint */ +export function IsBigInt(value: unknown): value is bigint { + return typeof value === 'bigint' +} +/** Returns true if this value is string */ +export function IsString(value: unknown): value is string { + return typeof value === 'string' +} +/** Returns true if this value is a function */ +export function IsFunction(value: unknown): value is Function { + return typeof value === 'function' +} +/** Returns true if this value is a symbol */ +export function IsSymbol(value: unknown): value is symbol { + return typeof value === 'symbol' +} +/** Returns true if this value is a value type such as number, string, boolean */ +export function IsValueType(value: unknown): value is ValueType { + // prettier-ignore + return ( + IsBigInt(value) || + IsBoolean(value) || + IsNull(value) || + IsNumber(value) || + IsString(value) || + IsSymbol(value) || + IsUndefined(value) + ) +} diff --git a/src/value/guard/index.ts b/src/value/guard/index.ts new file mode 100644 index 000000000..d00b7dc7b --- /dev/null +++ b/src/value/guard/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/value + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './guard' diff --git a/src/value/hash/hash.ts b/src/value/hash/hash.ts new file mode 100644 index 000000000..b72c039ea --- /dev/null +++ b/src/value/hash/hash.ts @@ -0,0 +1,162 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/value + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { IsArray, IsBoolean, IsBigInt, IsDate, IsNull, IsNumber, IsObject, IsString, IsSymbol, IsUint8Array, IsUndefined } from '../guard/index' +import { TypeBoxError } from '../../type/error/index' + +// ------------------------------------------------------------------ +// Errors +// ------------------------------------------------------------------ +export class ValueHashError extends TypeBoxError { + constructor(public readonly value: unknown) { + super(`Unable to hash value`) + } +} +// ------------------------------------------------------------------ +// ByteMarker +// ------------------------------------------------------------------ +enum ByteMarker { + Undefined, + Null, + Boolean, + Number, + String, + Object, + Array, + Date, + Uint8Array, + Symbol, + BigInt, +} +// ------------------------------------------------------------------ +// State +// ------------------------------------------------------------------ +let Accumulator = BigInt('14695981039346656037') +const [Prime, Size] = [BigInt('1099511628211'), BigInt('18446744073709551616' /* 2 ^ 64 */)] +const Bytes = Array.from({ length: 256 }).map((_, i) => BigInt(i)) +const F64 = new Float64Array(1) +const F64In = new DataView(F64.buffer) +const F64Out = new Uint8Array(F64.buffer) +// ------------------------------------------------------------------ +// NumberToBytes +// ------------------------------------------------------------------ +function* NumberToBytes(value: number): IterableIterator { + const byteCount = value === 0 ? 1 : Math.ceil(Math.floor(Math.log2(value) + 1) / 8) + for (let i = 0; i < byteCount; i++) { + yield (value >> (8 * (byteCount - 1 - i))) & 0xff + } +} +// ------------------------------------------------------------------ +// Hashing Functions +// ------------------------------------------------------------------ +function ArrayType(value: Array) { + FNV1A64(ByteMarker.Array) + for (const item of value) { + Visit(item) + } +} +function BooleanType(value: boolean) { + FNV1A64(ByteMarker.Boolean) + FNV1A64(value ? 1 : 0) +} +function BigIntType(value: bigint) { + FNV1A64(ByteMarker.BigInt) + F64In.setBigInt64(0, value) + for (const byte of F64Out) { + FNV1A64(byte) + } +} +function DateType(value: Date) { + FNV1A64(ByteMarker.Date) + Visit(value.getTime()) +} +function NullType(value: null) { + FNV1A64(ByteMarker.Null) +} +function NumberType(value: number) { + FNV1A64(ByteMarker.Number) + F64In.setFloat64(0, value) + for (const byte of F64Out) { + FNV1A64(byte) + } +} +function ObjectType(value: Record) { + FNV1A64(ByteMarker.Object) + for (const key of globalThis.Object.getOwnPropertyNames(value).sort()) { + Visit(key) + Visit(value[key]) + } +} +function StringType(value: string) { + FNV1A64(ByteMarker.String) + for (let i = 0; i < value.length; i++) { + for (const byte of NumberToBytes(value.charCodeAt(i))) { + FNV1A64(byte) + } + } +} +function SymbolType(value: symbol) { + FNV1A64(ByteMarker.Symbol) + Visit(value.description) +} +function Uint8ArrayType(value: Uint8Array) { + FNV1A64(ByteMarker.Uint8Array) + for (let i = 0; i < value.length; i++) { + FNV1A64(value[i]) + } +} +function UndefinedType(value: undefined) { + return FNV1A64(ByteMarker.Undefined) +} +function Visit(value: any) { + if (IsArray(value)) return ArrayType(value) + if (IsBoolean(value)) return BooleanType(value) + if (IsBigInt(value)) return BigIntType(value) + if (IsDate(value)) return DateType(value) + if (IsNull(value)) return NullType(value) + if (IsNumber(value)) return NumberType(value) + if (IsObject(value)) return ObjectType(value) + if (IsString(value)) return StringType(value) + if (IsSymbol(value)) return SymbolType(value) + if (IsUint8Array(value)) return Uint8ArrayType(value) + if (IsUndefined(value)) return UndefinedType(value) + throw new ValueHashError(value) +} +function FNV1A64(byte: number) { + Accumulator = Accumulator ^ Bytes[byte] + Accumulator = (Accumulator * Prime) % Size +} +// ------------------------------------------------------------------ +// Hash +// ------------------------------------------------------------------ +/** Creates a FNV1A-64 non cryptographic hash of the given value */ +export function Hash(value: unknown) { + Accumulator = BigInt('14695981039346656037') + Visit(value) + return Accumulator +} diff --git a/src/value/hash/index.ts b/src/value/hash/index.ts new file mode 100644 index 000000000..c7122183b --- /dev/null +++ b/src/value/hash/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/value + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './hash' diff --git a/src/value/index.ts b/src/value/index.ts new file mode 100644 index 000000000..0f91d6e9f --- /dev/null +++ b/src/value/index.ts @@ -0,0 +1,60 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/value + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +// ------------------------------------------------------------------ +// Errors (re-export) +// ------------------------------------------------------------------ +export { ValueError, ValueErrorType, ValueErrorIterator } from '../errors/index' +// ------------------------------------------------------------------ +// Guards +// ------------------------------------------------------------------ +export * from './guard/index' +// ------------------------------------------------------------------ +// Operators +// ------------------------------------------------------------------ +export * from './assert/index' +export * from './cast/index' +export * from './check/index' +export * from './clean/index' +export * from './clone/index' +export * from './convert/index' +export * from './create/index' +export * from './decode/index' +export * from './default/index' +export * from './delta/index' +export * from './encode/index' +export * from './equal/index' +export * from './hash/index' +export * from './mutate/index' +export * from './parse/index' +export * from './pointer/index' +export * from './transform/index' +// ------------------------------------------------------------------ +// Namespace +// ------------------------------------------------------------------ +export { Value } from './value/index' diff --git a/src/value/mutate/index.ts b/src/value/mutate/index.ts new file mode 100644 index 000000000..d999d10eb --- /dev/null +++ b/src/value/mutate/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/value + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './mutate' diff --git a/src/value/mutate/mutate.ts b/src/value/mutate/mutate.ts new file mode 100644 index 000000000..5b7128dc4 --- /dev/null +++ b/src/value/mutate/mutate.ts @@ -0,0 +1,123 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/value + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { IsObject, IsArray, IsTypedArray, IsValueType, type TypedArrayType } from '../guard/index' +import { ValuePointer } from '../pointer/index' +import { Clone } from '../clone/index' +import { TypeBoxError } from '../../type/error/index' + +// ------------------------------------------------------------------ +// IsStandardObject +// ------------------------------------------------------------------ +function IsStandardObject(value: unknown): value is Record { + return IsObject(value) && !IsArray(value) +} +// ------------------------------------------------------------------ +// Errors +// ------------------------------------------------------------------ +export class ValueMutateError extends TypeBoxError { + constructor(message: string) { + super(message) + } +} +// ------------------------------------------------------------------ +// Mutators +// ------------------------------------------------------------------ +export type Mutable = { [key: string]: unknown } | unknown[] +function ObjectType(root: Mutable, path: string, current: unknown, next: Record) { + if (!IsStandardObject(current)) { + ValuePointer.Set(root, path, Clone(next)) + } else { + const currentKeys = Object.getOwnPropertyNames(current) + const nextKeys = Object.getOwnPropertyNames(next) + for (const currentKey of currentKeys) { + if (!nextKeys.includes(currentKey)) { + delete current[currentKey] + } + } + for (const nextKey of nextKeys) { + if (!currentKeys.includes(nextKey)) { + current[nextKey] = null + } + } + for (const nextKey of nextKeys) { + Visit(root, `${path}/${nextKey}`, current[nextKey], next[nextKey]) + } + } +} +function ArrayType(root: Mutable, path: string, current: unknown, next: unknown[]) { + if (!IsArray(current)) { + ValuePointer.Set(root, path, Clone(next)) + } else { + for (let index = 0; index < next.length; index++) { + Visit(root, `${path}/${index}`, current[index], next[index]) + } + current.splice(next.length) + } +} +function TypedArrayType(root: Mutable, path: string, current: unknown, next: TypedArrayType) { + if (IsTypedArray(current) && current.length === next.length) { + for (let i = 0; i < current.length; i++) { + current[i] = next[i] + } + } else { + ValuePointer.Set(root, path, Clone(next)) + } +} +function ValueType(root: Mutable, path: string, current: unknown, next: unknown) { + if (current === next) return + ValuePointer.Set(root, path, next) +} +function Visit(root: Mutable, path: string, current: unknown, next: unknown) { + if (IsArray(next)) return ArrayType(root, path, current, next) + if (IsTypedArray(next)) return TypedArrayType(root, path, current, next) + if (IsStandardObject(next)) return ObjectType(root, path, current, next) + if (IsValueType(next)) return ValueType(root, path, current, next) +} +// ------------------------------------------------------------------ +// IsNonMutableValue +// ------------------------------------------------------------------ +function IsNonMutableValue(value: unknown): value is Mutable { + return IsTypedArray(value) || IsValueType(value) +} +function IsMismatchedValue(current: unknown, next: unknown) { + // prettier-ignore + return ( + (IsStandardObject(current) && IsArray(next)) || + (IsArray(current) && IsStandardObject(next)) + ) +} +// ------------------------------------------------------------------ +// Mutate +// ------------------------------------------------------------------ +/** `[Mutable]` Performs a deep mutable value assignment while retaining internal references */ +export function Mutate(current: Mutable, next: Mutable): void { + if (IsNonMutableValue(current) || IsNonMutableValue(next)) throw new ValueMutateError('Only object and array types can be mutated at the root level') + if (IsMismatchedValue(current, next)) throw new ValueMutateError('Cannot assign due type mismatch of assignable values') + Visit(current, '', current, next) +} diff --git a/src/value/parse/index.ts b/src/value/parse/index.ts new file mode 100644 index 000000000..a5f6ba890 --- /dev/null +++ b/src/value/parse/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/value + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './parse' diff --git a/src/value/parse/parse.ts b/src/value/parse/parse.ts new file mode 100644 index 000000000..429b0084c --- /dev/null +++ b/src/value/parse/parse.ts @@ -0,0 +1,130 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/value + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { TypeBoxError } from '../../type/error/index' +import { TransformDecode, TransformEncode, HasTransform } from '../transform/index' +import { TSchema } from '../../type/schema/index' +import { StaticDecode } from '../../type/static/index' +import { Assert } from '../assert/index' +import { Cast } from '../cast/index' +import { Clean } from '../clean/index' +import { Clone } from '../clone/index' +import { Convert } from '../convert/index' +import { Default } from '../default/index' + +// ------------------------------------------------------------------ +// Guards +// ------------------------------------------------------------------ +import { IsArray, IsUndefined } from '../guard/index' + +// ------------------------------------------------------------------ +// Error +// ------------------------------------------------------------------ +export class ParseError extends TypeBoxError { + constructor(message: string) { + super(message) + } +} + +// ------------------------------------------------------------------ +// ParseRegistry +// ------------------------------------------------------------------ +export type TParseOperation = 'Assert' | 'Cast' | 'Clean' | 'Clone' | 'Convert' | 'Decode' | 'Default' | 'Encode' | ({} & string) +export type TParseFunction = (type: TSchema, references: TSchema[], value: unknown) => unknown + +// prettier-ignore +export namespace ParseRegistry { + const registry = new Map([ + ['Assert', (type, references, value: unknown) => { Assert(type, references, value); return value }], + ['Cast', (type, references, value: unknown) => Cast(type, references, value)], + ['Clean', (type, references, value: unknown) => Clean(type, references, value)], + ['Clone', (_type, _references, value: unknown) => Clone(value)], + ['Convert', (type, references, value: unknown) => Convert(type, references, value)], + ['Decode', (type, references, value: unknown) => (HasTransform(type, references) ? TransformDecode(type, references, value) : value)], + ['Default', (type, references, value: unknown) => Default(type, references, value)], + ['Encode', (type, references, value: unknown) => (HasTransform(type, references) ? TransformEncode(type, references, value) : value)], + ]) + // Deletes an entry from the registry + export function Delete(key: string): void { + registry.delete(key) + } + // Sets an entry in the registry + export function Set(key: string, callback: TParseFunction): void { + registry.set(key, callback) + } + // Gets an entry in the registry + export function Get(key: string): TParseFunction | undefined { + return registry.get(key) + } +} +// ------------------------------------------------------------------ +// Default Parse Pipeline +// ------------------------------------------------------------------ +// prettier-ignore +export const ParseDefault = [ + 'Clone', + 'Clean', + 'Default', + 'Convert', + 'Assert', + 'Decode' +] as const + +// ------------------------------------------------------------------ +// ParseValue +// ------------------------------------------------------------------ +function ParseValue = StaticDecode>(operations: TParseOperation[], type: Type, references: TSchema[], value: unknown): Result { + return operations.reduce((value, operationKey) => { + const operation = ParseRegistry.Get(operationKey) + if (IsUndefined(operation)) throw new ParseError(`Unable to find Parse operation '${operationKey}'`) + return operation(type, references, value) + }, value) as Result +} + +// ------------------------------------------------------------------ +// Parse +// ------------------------------------------------------------------ +/** Parses a value using the default parse pipeline. Will throws an `AssertError` if invalid. */ +export function Parse, Result extends Output = Output>(schema: Type, references: TSchema[], value: unknown): Result +/** Parses a value using the default parse pipeline. Will throws an `AssertError` if invalid. */ +export function Parse, Result extends Output = Output>(schema: Type, value: unknown): Result +/** Parses a value using the specified operations. */ +export function Parse(operations: TParseOperation[], schema: Type, references: TSchema[], value: unknown): unknown +/** Parses a value using the specified operations. */ +export function Parse(operations: TParseOperation[], schema: Type, value: unknown): unknown +/** Parses a value */ +export function Parse(...args: any[]): unknown { + // prettier-ignore + const [operations, schema, references, value] = ( + args.length === 4 ? [args[0], args[1], args[2], args[3]] : + args.length === 3 ? IsArray(args[0]) ? [args[0], args[1], [], args[2]] : [ParseDefault, args[0], args[1], args[2]] : + args.length === 2 ? [ParseDefault, args[0], [], args[1]] : + (() => { throw new ParseError('Invalid Arguments') })() + ) + return ParseValue(operations, schema, references, value) +} diff --git a/src/value/pointer/index.ts b/src/value/pointer/index.ts new file mode 100644 index 000000000..f0828dcb6 --- /dev/null +++ b/src/value/pointer/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/value + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * as ValuePointer from './pointer' diff --git a/src/value/pointer/pointer.ts b/src/value/pointer/pointer.ts new file mode 100644 index 000000000..aa1b7e39c --- /dev/null +++ b/src/value/pointer/pointer.ts @@ -0,0 +1,127 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/value + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { TypeBoxError } from '../../type/error/index' + +// ------------------------------------------------------------------ +// Errors +// ------------------------------------------------------------------ +export class ValuePointerRootSetError extends TypeBoxError { + constructor(public readonly value: unknown, public readonly path: string, public readonly update: unknown) { + super('Cannot set root value') + } +} +export class ValuePointerRootDeleteError extends TypeBoxError { + constructor(public readonly value: unknown, public readonly path: string) { + super('Cannot delete root value') + } +} +// ------------------------------------------------------------------ +// ValuePointer +// ------------------------------------------------------------------ +/** Provides functionality to update values through RFC6901 string pointers */ +// prettier-ignore +function Escape(component: string) { + return component.indexOf('~') === -1 ? component : component.replace(/~1/g, '/').replace(/~0/g, '~') +} +/** Formats the given pointer into navigable key components */ +// prettier-ignore +export function* Format(pointer: string): IterableIterator { + if (pointer === '') return + let [start, end] = [0, 0] + for (let i = 0; i < pointer.length; i++) { + const char = pointer.charAt(i) + if (char === '/') { + if (i === 0) { + start = i + 1 + } else { + end = i + yield Escape(pointer.slice(start, end)) + start = i + 1 + } + } else { + end = i + } + } + yield Escape(pointer.slice(start)) +} +/** Sets the value at the given pointer. If the value at the pointer does not exist it is created */ +// prettier-ignore +export function Set(value: any, pointer: string, update: unknown): void { + if (pointer === '') throw new ValuePointerRootSetError(value, pointer, update) + let [owner, next, key] = [null as any, value, ''] + for (const component of Format(pointer)) { + if (next[component] === undefined) next[component] = {} + owner = next + next = next[component] + key = component + } + owner[key] = update +} +/** Deletes a value at the given pointer */ +// prettier-ignore +export function Delete(value: any, pointer: string): void { + if (pointer === '') throw new ValuePointerRootDeleteError(value, pointer) + let [owner, next, key] = [null as any, value as any, ''] + for (const component of Format(pointer)) { + if (next[component] === undefined || next[component] === null) return + owner = next + next = next[component] + key = component + } + if (Array.isArray(owner)) { + const index = parseInt(key) + owner.splice(index, 1) + } else { + delete owner[key] + } +} +/** Returns true if a value exists at the given pointer */ +// prettier-ignore +export function Has(value: any, pointer: string): boolean { + if (pointer === '') return true + let [owner, next, key] = [null as any, value as any, ''] + for (const component of Format(pointer)) { + if (next[component] === undefined) return false + owner = next + next = next[component] + key = component + } + return Object.getOwnPropertyNames(owner).includes(key) +} +/** Gets the value at the given pointer */ +// prettier-ignore +export function Get(value: any, pointer: string): any { + if (pointer === '') return value + let current = value + for (const component of Format(pointer)) { + if (current[component] === undefined) return undefined + current = current[component] + } + return current +} diff --git a/src/value/transform/decode.ts b/src/value/transform/decode.ts new file mode 100644 index 000000000..c40752ec1 --- /dev/null +++ b/src/value/transform/decode.ts @@ -0,0 +1,242 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/value + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { TypeSystemPolicy } from '../../system/policy' +import { Kind, TransformKind } from '../../type/symbols/index' +import { TypeBoxError } from '../../type/error/index' +import { ValueError } from '../../errors/index' +import { KeyOfPropertyKeys, KeyOfPropertyEntries } from '../../type/keyof/index' +import { Deref, Pushref } from '../deref/index' +import { Check } from '../check/index' + +import type { TSchema } from '../../type/schema/index' +import type { TArray } from '../../type/array/index' +import type { TImport } from '../../type/module/index' +import type { TIntersect } from '../../type/intersect/index' +import type { TNot } from '../../type/not/index' +import type { TObject } from '../../type/object/index' +import type { TRecord } from '../../type/record/index' +import type { TRef } from '../../type/ref/index' +import type { TThis } from '../../type/recursive/index' +import type { TTuple } from '../../type/tuple/index' +import type { TUnion } from '../../type/union/index' + +// ------------------------------------------------------------------ +// ValueGuard +// ------------------------------------------------------------------ +import { HasPropertyKey, IsObject, IsArray, IsValueType, IsUndefined as IsUndefinedValue } from '../guard/index' +// ------------------------------------------------------------------ +// KindGuard +// ------------------------------------------------------------------ +import { IsTransform, IsSchema, IsUndefined } from '../../type/guard/kind' +// ------------------------------------------------------------------ +// Errors +// ------------------------------------------------------------------ +// thrown externally +// prettier-ignore +export class TransformDecodeCheckError extends TypeBoxError { + constructor( + public readonly schema: TSchema, + public readonly value: unknown, + public readonly error: ValueError + ) { + super(`Unable to decode value as it does not match the expected schema`) + } +} +// prettier-ignore +export class TransformDecodeError extends TypeBoxError { + constructor( + public readonly schema: TSchema, + public readonly path: string, + public readonly value: unknown, + public readonly error: Error, + ) { + super(error instanceof Error ? error.message : 'Unknown error') + } +} +// ------------------------------------------------------------------ +// Decode +// ------------------------------------------------------------------ +// prettier-ignore +function Default(schema: TSchema, path: string, value: any): unknown { + try { + return IsTransform(schema) ? schema[TransformKind].Decode(value) : value + } catch (error) { + throw new TransformDecodeError(schema, path, value, error as Error) + } +} +// prettier-ignore +function FromArray(schema: TArray, references: TSchema[], path: string, value: any): unknown { + return (IsArray(value)) + ? Default(schema, path, value.map((value: any, index) => Visit(schema.items, references, `${path}/${index}`, value))) + : Default(schema, path, value) +} +// prettier-ignore +function FromIntersect(schema: TIntersect, references: TSchema[], path: string, value: any): unknown { + if (!IsObject(value) || IsValueType(value)) return Default(schema, path, value) + const knownEntries = KeyOfPropertyEntries(schema) + const knownKeys = knownEntries.map(entry => entry[0]) + const knownProperties = { ...value } as Record + for(const [knownKey, knownSchema] of knownEntries) if(knownKey in knownProperties) { + knownProperties[knownKey] = Visit(knownSchema, references, `${path}/${knownKey}`, knownProperties[knownKey]) + } + if (!IsTransform(schema.unevaluatedProperties)) { + return Default(schema, path, knownProperties) + } + const unknownKeys = Object.getOwnPropertyNames(knownProperties) + const unevaluatedProperties = schema.unevaluatedProperties as TSchema + const unknownProperties = { ...knownProperties } as Record + for(const key of unknownKeys) if(!knownKeys.includes(key)) { + unknownProperties[key] = Default(unevaluatedProperties, `${path}/${key}`, unknownProperties[key]) + } + return Default(schema, path, unknownProperties) +} +// prettier-ignore +function FromImport(schema: TImport, references: TSchema[], path: string, value: unknown): unknown { + const additional = globalThis.Object.values(schema.$defs) as TSchema[] + const target = schema.$defs[schema.$ref] as TSchema + const result = Visit(target, [...references, ...additional], path, value) + return Default(schema, path, result) +} +function FromNot(schema: TNot, references: TSchema[], path: string, value: any): unknown { + return Default(schema, path, Visit(schema.not, references, path, value)) +} +// prettier-ignore +function FromObject(schema: TObject, references: TSchema[], path: string, value: any): unknown { + if (!IsObject(value)) return Default(schema, path, value) + const knownKeys = KeyOfPropertyKeys(schema) as string[] + const knownProperties = { ...value } as Record + for(const key of knownKeys) { + if(!HasPropertyKey(knownProperties, key)) continue + // if the property value is undefined, but the target is not, nor does it satisfy exact optional + // property policy, then we need to continue. This is a special case for optional property handling + // where a transforms wrapped in a optional modifiers should not run. + if(IsUndefinedValue(knownProperties[key]) && ( + !IsUndefined(schema.properties[key]) || + TypeSystemPolicy.IsExactOptionalProperty(knownProperties, key) + )) continue + // decode property + knownProperties[key] = Visit(schema.properties[key], references, `${path}/${key}`, knownProperties[key]) + } + if (!IsSchema(schema.additionalProperties)) { + return Default(schema, path, knownProperties) + } + const unknownKeys = Object.getOwnPropertyNames(knownProperties) + const additionalProperties = schema.additionalProperties as TSchema + const unknownProperties = { ...knownProperties } as Record + for(const key of unknownKeys) if(!knownKeys.includes(key)) { + unknownProperties[key] = Default(additionalProperties, `${path}/${key}`, unknownProperties[key]) + } + return Default(schema, path, unknownProperties) +} +// prettier-ignore +function FromRecord(schema: TRecord, references: TSchema[], path: string, value: any): unknown { + if (!IsObject(value)) return Default(schema, path, value) + const pattern = Object.getOwnPropertyNames(schema.patternProperties)[0] + const knownKeys = new RegExp(pattern) + const knownProperties = { ...value } as Record + for(const key of Object.getOwnPropertyNames(value)) if(knownKeys.test(key)) { + knownProperties[key] = Visit(schema.patternProperties[pattern], references, `${path}/${key}`, knownProperties[key]) + } + if (!IsSchema(schema.additionalProperties)) { + return Default(schema, path, knownProperties) + } + const unknownKeys = Object.getOwnPropertyNames(knownProperties) + const additionalProperties = schema.additionalProperties as TSchema + const unknownProperties = {...knownProperties} as Record + for(const key of unknownKeys) if(!knownKeys.test(key)) { + unknownProperties[key] = Default(additionalProperties, `${path}/${key}`, unknownProperties[key]) + } + return Default(schema, path, unknownProperties) +} +// prettier-ignore +function FromRef(schema: TRef, references: TSchema[], path: string, value: any): unknown { + const target = Deref(schema, references) + return Default(schema, path, Visit(target, references, path, value)) +} +// prettier-ignore +function FromThis(schema: TThis, references: TSchema[], path: string, value: any): unknown { + const target = Deref(schema, references) + return Default(schema, path, Visit(target, references, path, value)) +} +// prettier-ignore +function FromTuple(schema: TTuple, references: TSchema[], path: string, value: any): unknown { + return (IsArray(value) && IsArray(schema.items)) + ? Default(schema, path, schema.items.map((schema, index) => Visit(schema, references, `${path}/${index}`, value[index]))) + : Default(schema, path, value) +} +// prettier-ignore +function FromUnion(schema: TUnion, references: TSchema[], path: string, value: any): unknown { + for (const subschema of schema.anyOf) { + if (!Check(subschema, references, value)) continue + // note: ensure interior is decoded first + const decoded = Visit(subschema, references, path, value) + return Default(schema, path, decoded) + } + return Default(schema, path, value) +} + +// prettier-ignore +function Visit(schema: TSchema, references: TSchema[], path: string, value: any): any { + const references_ = Pushref(schema, references) + const schema_ = schema as any + switch (schema[Kind]) { + case 'Array': + return FromArray(schema_, references_, path, value) + case 'Import': + return FromImport(schema_, references_, path, value) + case 'Intersect': + return FromIntersect(schema_, references_, path, value) + case 'Not': + return FromNot(schema_, references_, path, value) + case 'Object': + return FromObject(schema_, references_, path, value) + case 'Record': + return FromRecord(schema_, references_, path, value) + case 'Ref': + return FromRef(schema_, references_, path, value) + case 'Symbol': + return Default(schema_, path, value) + case 'This': + return FromThis(schema_, references_, path, value) + case 'Tuple': + return FromTuple(schema_, references_, path, value) + case 'Union': + return FromUnion(schema_, references_, path, value) + default: + return Default(schema_, path, value) + } +} +/** + * `[Internal]` Decodes the value and returns the result. This function requires that + * the caller `Check` the value before use. Passing unchecked values may result in + * undefined behavior. Refer to the `Value.Decode()` for implementation details. + */ +export function TransformDecode(schema: TSchema, references: TSchema[], value: unknown): unknown { + return Visit(schema, references, '', value) +} diff --git a/src/value/transform/encode.ts b/src/value/transform/encode.ts new file mode 100644 index 000000000..ab8de080f --- /dev/null +++ b/src/value/transform/encode.ts @@ -0,0 +1,251 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/value + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { TypeSystemPolicy } from '../../system/policy' +import { Kind, TransformKind } from '../../type/symbols/index' +import { TypeBoxError } from '../../type/error/index' +import { ValueError } from '../../errors/index' +import { KeyOfPropertyKeys, KeyOfPropertyEntries } from '../../type/keyof/index' +import { Deref, Pushref } from '../deref/index' +import { Check } from '../check/index' + +import type { TSchema } from '../../type/schema/index' +import type { TArray } from '../../type/array/index' +import type { TImport } from '../../type/module/index' +import type { TIntersect } from '../../type/intersect/index' +import type { TNot } from '../../type/not/index' +import type { TObject } from '../../type/object/index' +import type { TRecord } from '../../type/record/index' +import type { TRef } from '../../type/ref/index' +import type { TThis } from '../../type/recursive/index' +import type { TTuple } from '../../type/tuple/index' +import type { TUnion } from '../../type/union/index' + +// ------------------------------------------------------------------ +// ValueGuard +// ------------------------------------------------------------------ +import { HasPropertyKey, IsObject, IsArray, IsValueType, IsUndefined as IsUndefinedValue } from '../guard/index' +// ------------------------------------------------------------------ +// KindGuard +// ------------------------------------------------------------------ +import { IsTransform, IsSchema, IsUndefined } from '../../type/guard/kind' +// ------------------------------------------------------------------ +// Errors +// ------------------------------------------------------------------ +// prettier-ignore +export class TransformEncodeCheckError extends TypeBoxError { + constructor( + public readonly schema: TSchema, + public readonly value: unknown, + public readonly error: ValueError + ) { + super(`The encoded value does not match the expected schema`) + } +} +// prettier-ignore +export class TransformEncodeError extends TypeBoxError { + constructor( + public readonly schema: TSchema, + public readonly path: string, + public readonly value: unknown, + public readonly error: Error, + ) { + super(`${error instanceof Error ? error.message : 'Unknown error'}`) + } +} +// ------------------------------------------------------------------ +// Encode +// ------------------------------------------------------------------ +// prettier-ignore +function Default(schema: TSchema, path: string, value: any) { + try { + return IsTransform(schema) ? schema[TransformKind].Encode(value) : value + } catch (error) { + throw new TransformEncodeError(schema, path, value, error as Error) + } +} +// prettier-ignore +function FromArray(schema: TArray, references: TSchema[], path: string, value: any): any { + const defaulted = Default(schema, path, value) + return IsArray(defaulted) + ? defaulted.map((value: any, index) => Visit(schema.items, references, `${path}/${index}`, value)) + : defaulted +} +// prettier-ignore +function FromImport(schema: TImport, references: TSchema[], path: string, value: unknown): unknown { + const additional = globalThis.Object.values(schema.$defs) as TSchema[] + const target = schema.$defs[schema.$ref] as TSchema + const result = Default(schema, path, value) + return Visit(target, [...references, ...additional], path, result) +} +// prettier-ignore +function FromIntersect(schema: TIntersect, references: TSchema[], path: string, value: any) { + const defaulted = Default(schema, path, value) + if (!IsObject(value) || IsValueType(value)) return defaulted + const knownEntries = KeyOfPropertyEntries(schema) + const knownKeys = knownEntries.map(entry => entry[0]) + const knownProperties = { ...defaulted } as Record + for(const [knownKey, knownSchema] of knownEntries) if(knownKey in knownProperties) { + knownProperties[knownKey] = Visit(knownSchema, references, `${path}/${knownKey}`, knownProperties[knownKey]) + } + if (!IsTransform(schema.unevaluatedProperties)) { + return knownProperties + } + const unknownKeys = Object.getOwnPropertyNames(knownProperties) + const unevaluatedProperties = schema.unevaluatedProperties as TSchema + const properties = { ...knownProperties } as Record + for(const key of unknownKeys) if(!knownKeys.includes(key)) { + properties[key] = Default(unevaluatedProperties, `${path}/${key}`, properties[key]) + } + return properties +} +// prettier-ignore +function FromNot(schema: TNot, references: TSchema[], path: string, value: any) { + return Default(schema.not, path, Default(schema, path, value)) +} +// prettier-ignore +function FromObject(schema: TObject, references: TSchema[], path: string, value: any) { + const defaulted = Default(schema, path, value) + if (!IsObject(defaulted)) return defaulted + const knownKeys = KeyOfPropertyKeys(schema) as string[] + const knownProperties = { ...defaulted } as Record + for(const key of knownKeys) { + if(!HasPropertyKey(knownProperties, key)) continue + // if the property value is undefined, but the target is not, nor does it satisfy exact optional + // property policy, then we need to continue. This is a special case for optional property handling + // where a transforms wrapped in a optional modifiers should not run. + if(IsUndefinedValue(knownProperties[key]) && ( + !IsUndefined(schema.properties[key]) || + TypeSystemPolicy.IsExactOptionalProperty(knownProperties, key) + )) continue + // encode property + knownProperties[key] = Visit(schema.properties[key], references, `${path}/${key}`, knownProperties[key]) + } + if (!IsSchema(schema.additionalProperties)) { + return knownProperties + } + const unknownKeys = Object.getOwnPropertyNames(knownProperties) + const additionalProperties = schema.additionalProperties as TSchema + const properties = { ...knownProperties } as Record + for(const key of unknownKeys) if(!knownKeys.includes(key)) { + properties[key] = Default(additionalProperties, `${path}/${key}`, properties[key]) + } + return properties +} +// prettier-ignore +function FromRecord(schema: TRecord, references: TSchema[], path: string, value: any) { + const defaulted = Default(schema, path, value) as Record + if (!IsObject(value)) return defaulted + const pattern = Object.getOwnPropertyNames(schema.patternProperties)[0] + const knownKeys = new RegExp(pattern) + const knownProperties = {...defaulted } as Record + for(const key of Object.getOwnPropertyNames(value)) if(knownKeys.test(key)) { + knownProperties[key] = Visit(schema.patternProperties[pattern], references, `${path}/${key}`, knownProperties[key]) + } + if (!IsSchema(schema.additionalProperties)) { + return knownProperties + } + const unknownKeys = Object.getOwnPropertyNames(knownProperties) + const additionalProperties = schema.additionalProperties as TSchema + const properties = { ...knownProperties } as Record + for(const key of unknownKeys) if(!knownKeys.test(key)) { + properties[key] = Default(additionalProperties, `${path}/${key}`, properties[key]) + } + return properties +} +// prettier-ignore +function FromRef(schema: TRef, references: TSchema[], path: string, value: any) { + const target = Deref(schema, references) + const resolved = Visit(target, references, path, value) + return Default(schema, path, resolved) +} +// prettier-ignore +function FromThis(schema: TThis, references: TSchema[], path: string, value: any) { + const target = Deref(schema, references) + const resolved = Visit(target, references, path, value) + return Default(schema, path, resolved) +} +// prettier-ignore +function FromTuple(schema: TTuple, references: TSchema[], path: string, value: any) { + const value1 = Default(schema, path, value) + return IsArray(schema.items) ? schema.items.map((schema, index) => Visit(schema, references, `${path}/${index}`, value1[index])) : [] +} +// prettier-ignore +function FromUnion(schema: TUnion, references: TSchema[], path: string, value: any) { + // test value against union variants + for (const subschema of schema.anyOf) { + if (!Check(subschema, references, value)) continue + const value1 = Visit(subschema, references, path, value) + return Default(schema, path, value1) + } + // test transformed value against union variants + for (const subschema of schema.anyOf) { + const value1 = Visit(subschema, references, path, value) + if (!Check(schema, references, value1)) continue + return Default(schema, path, value1) + } + return Default(schema, path, value) +} +// prettier-ignore +function Visit(schema: TSchema, references: TSchema[], path: string, value: any): any { + const references_ = Pushref(schema, references) + const schema_ = schema as any + switch (schema[Kind]) { + case 'Array': + return FromArray(schema_, references_, path, value) + case 'Import': + return FromImport(schema_, references_, path, value) + case 'Intersect': + return FromIntersect(schema_, references_, path, value) + case 'Not': + return FromNot(schema_, references_, path, value) + case 'Object': + return FromObject(schema_, references_, path, value) + case 'Record': + return FromRecord(schema_, references_, path, value) + case 'Ref': + return FromRef(schema_, references_, path, value) + case 'This': + return FromThis(schema_, references_, path, value) + case 'Tuple': + return FromTuple(schema_, references_, path, value) + case 'Union': + return FromUnion(schema_, references_, path, value) + default: + return Default(schema_, path, value) + } +} +/** + * `[Internal]` Encodes the value and returns the result. This function expects the + * caller to pass a statically checked value. This function does not check the encoded + * result, meaning the result should be passed to `Check` before use. Refer to the + * `Value.Encode()` function for implementation details. + */ +export function TransformEncode(schema: TSchema, references: TSchema[], value: unknown): unknown { + return Visit(schema, references, '', value) +} diff --git a/src/value/transform/has.ts b/src/value/transform/has.ts new file mode 100644 index 000000000..0acf959c1 --- /dev/null +++ b/src/value/transform/has.ts @@ -0,0 +1,176 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/value + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { Deref, Pushref } from '../deref/index' +import { Kind } from '../../type/symbols/index' + +import type { TSchema } from '../../type/schema/index' +import type { TArray } from '../../type/array/index' +import type { TAsyncIterator } from '../../type/async-iterator/index' +import type { TConstructor } from '../../type/constructor/index' +import type { TFunction } from '../../type/function/index' +import type { TIntersect } from '../../type/intersect/index' +import type { TIterator } from '../../type/iterator/index' +import type { TImport } from '../../type/module/index' +import type { TNot } from '../../type/not/index' +import type { TObject } from '../../type/object/index' +import type { TPromise } from '../../type/promise/index' +import type { TRecord } from '../../type/record/index' +import type { TRef } from '../../type/ref/index' +import type { TThis } from '../../type/recursive/index' +import type { TTuple } from '../../type/tuple/index' +import type { TUnion } from '../../type/union/index' + +// ------------------------------------------------------------------ +// KindGuard +// ------------------------------------------------------------------ +import { IsTransform, IsSchema } from '../../type/guard/kind' +// ------------------------------------------------------------------ +// ValueGuard +// ------------------------------------------------------------------ +import { IsUndefined } from '../guard/index' + +// prettier-ignore +function FromArray(schema: TArray, references: TSchema[]): boolean { + return IsTransform(schema) || Visit(schema.items, references) +} +// prettier-ignore +function FromAsyncIterator(schema: TAsyncIterator, references: TSchema[]): boolean { + return IsTransform(schema) || Visit(schema.items, references) +} +// prettier-ignore +function FromConstructor(schema: TConstructor, references: TSchema[]) { + return IsTransform(schema) || Visit(schema.returns, references) || schema.parameters.some((schema) => Visit(schema, references)) +} +// prettier-ignore +function FromFunction(schema: TFunction, references: TSchema[]) { + return IsTransform(schema) || Visit(schema.returns, references) || schema.parameters.some((schema) => Visit(schema, references)) +} +// prettier-ignore +function FromIntersect(schema: TIntersect, references: TSchema[]) { + return IsTransform(schema) || IsTransform(schema.unevaluatedProperties) || schema.allOf.some((schema) => Visit(schema, references)) +} +// prettier-ignore +function FromImport(schema: TImport, references: TSchema[]) { + const additional = globalThis.Object.getOwnPropertyNames(schema.$defs).reduce((result, key) => [...result, schema.$defs[key as never]], [] as TSchema[]) + const target = schema.$defs[schema.$ref] + return IsTransform(schema) || Visit(target, [...additional, ...references]) +} +// prettier-ignore +function FromIterator(schema: TIterator, references: TSchema[]) { + return IsTransform(schema) || Visit(schema.items, references) +} +// prettier-ignore +function FromNot(schema: TNot, references: TSchema[]) { + return IsTransform(schema) || Visit(schema.not, references) +} +// prettier-ignore +function FromObject(schema: TObject, references: TSchema[]) { + return ( + IsTransform(schema) || + Object.values(schema.properties).some((schema) => Visit(schema, references)) || + ( + IsSchema(schema.additionalProperties) && Visit(schema.additionalProperties, references) + ) + ) +} +// prettier-ignore +function FromPromise(schema: TPromise, references: TSchema[]) { + return IsTransform(schema) || Visit(schema.item, references) +} +// prettier-ignore +function FromRecord(schema: TRecord, references: TSchema[]) { + const pattern = Object.getOwnPropertyNames(schema.patternProperties)[0] + const property = schema.patternProperties[pattern] + return IsTransform(schema) || Visit(property, references) || (IsSchema(schema.additionalProperties) && IsTransform(schema.additionalProperties)) +} +// prettier-ignore +function FromRef(schema: TRef, references: TSchema[]) { + if (IsTransform(schema)) return true + return Visit(Deref(schema, references), references) +} +// prettier-ignore +function FromThis(schema: TThis, references: TSchema[]) { + if (IsTransform(schema)) return true + return Visit(Deref(schema, references), references) +} +// prettier-ignore +function FromTuple(schema: TTuple, references: TSchema[]) { + return IsTransform(schema) || (!IsUndefined(schema.items) && schema.items.some((schema) => Visit(schema, references))) +} +// prettier-ignore +function FromUnion(schema: TUnion, references: TSchema[]) { + return IsTransform(schema) || schema.anyOf.some((schema) => Visit(schema, references)) +} +// prettier-ignore +function Visit(schema: TSchema, references: TSchema[]): boolean { + const references_ = Pushref(schema, references) + const schema_ = schema as any + if (schema.$id && visited.has(schema.$id)) return false + if (schema.$id) visited.add(schema.$id) + switch (schema[Kind]) { + case 'Array': + return FromArray(schema_, references_) + case 'AsyncIterator': + return FromAsyncIterator(schema_, references_) + case 'Constructor': + return FromConstructor(schema_, references_) + case 'Function': + return FromFunction(schema_, references_) + case 'Import': + return FromImport(schema_, references_) + case 'Intersect': + return FromIntersect(schema_, references_) + case 'Iterator': + return FromIterator(schema_, references_) + case 'Not': + return FromNot(schema_, references_) + case 'Object': + return FromObject(schema_, references_) + case 'Promise': + return FromPromise(schema_, references_) + case 'Record': + return FromRecord(schema_, references_) + case 'Ref': + return FromRef(schema_, references_) + case 'This': + return FromThis(schema_, references_) + case 'Tuple': + return FromTuple(schema_, references_) + case 'Union': + return FromUnion(schema_, references_) + default: + return IsTransform(schema) + } +} +const visited = new Set() +/** Returns true if this schema contains a transform codec */ +export function HasTransform(schema: TSchema, references: TSchema[]): boolean { + visited.clear() + return Visit(schema, references) +} diff --git a/src/value/transform/index.ts b/src/value/transform/index.ts new file mode 100644 index 000000000..1759d820d --- /dev/null +++ b/src/value/transform/index.ts @@ -0,0 +1,31 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/value + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * from './decode' +export * from './encode' +export * from './has' diff --git a/src/value/value/index.ts b/src/value/value/index.ts new file mode 100644 index 000000000..af4861374 --- /dev/null +++ b/src/value/value/index.ts @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/value + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * as Value from './value' diff --git a/src/value/value/value.ts b/src/value/value/value.ts new file mode 100644 index 000000000..d1d5f6319 --- /dev/null +++ b/src/value/value/value.ts @@ -0,0 +1,45 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox/value + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export { Errors, ValueErrorIterator } from '../../errors/index' + +export { Assert } from '../assert/index' +export { Cast } from '../cast/index' +export { Check } from '../check/index' +export { Clean } from '../clean/index' +export { Clone } from '../clone/index' +export { Convert } from '../convert/index' +export { Create } from '../create/index' +export { Decode } from '../decode/index' +export { Default } from '../default/index' +export { Diff, Patch, Edit } from '../delta/index' +export { Encode } from '../encode/index' +export { Equal } from '../equal/index' +export { Hash } from '../hash/index' +export { Mutate, type Mutable } from '../mutate/index' +export { Parse } from '../parse/index' diff --git a/task/benchmark/compression/index.ts b/task/benchmark/compression/index.ts new file mode 100644 index 000000000..546145c70 --- /dev/null +++ b/task/benchmark/compression/index.ts @@ -0,0 +1,25 @@ +import { shell } from '@sinclair/hammer' +import { statSync, readdirSync } from 'fs' +import { basename, extname } from 'path' + +export async function measure(test: string) { + await shell(`hammer build task/benchmark/compression/module/${test}.ts --dist target/benchmark/compression`) + const compiled = statSync(`target/benchmark/compression/${test}.js`) + await shell(`hammer build task/benchmark/compression/module/${test}.ts --dist target/benchmark/compression --minify`) + const minified = statSync(`target/benchmark/compression/${test}.js`) + return { + test: test.padEnd(20), + compiled: `${(compiled.size / 1000).toFixed(1)} kb`.padStart(8), + minified: `${(minified.size / 1000).toFixed(1)} kb`.padStart(8), + ratio: compiled.size / minified.size, + } +} + +export async function compression() { + const tests = readdirSync('task/benchmark/compression/module').map((name) => basename(name, extname(name))) + const results = await Promise.all(tests.map((test) => measure(test))) + const present = results.reduce((acc, c) => { + return { ...acc, [c.test.replace(/-/g, '/')]: { Compiled: c.compiled, Minified: c.minified, Compression: `${c.ratio.toFixed(2)} x` } } + }, {}) + console.table(present) +} diff --git a/task/benchmark/compression/module/typebox-compiler.ts b/task/benchmark/compression/module/typebox-compiler.ts new file mode 100644 index 000000000..4d23bb2f2 --- /dev/null +++ b/task/benchmark/compression/module/typebox-compiler.ts @@ -0,0 +1,3 @@ +import { TypeCompiler } from '@sinclair/typebox/compiler' + +console.log(TypeCompiler) diff --git a/task/benchmark/compression/module/typebox-errors.ts b/task/benchmark/compression/module/typebox-errors.ts new file mode 100644 index 000000000..f1de81528 --- /dev/null +++ b/task/benchmark/compression/module/typebox-errors.ts @@ -0,0 +1,3 @@ +import * as Errors from '@sinclair/typebox/errors' + +console.log(Errors) diff --git a/task/benchmark/compression/module/typebox-syntax.ts b/task/benchmark/compression/module/typebox-syntax.ts new file mode 100644 index 000000000..311871c68 --- /dev/null +++ b/task/benchmark/compression/module/typebox-syntax.ts @@ -0,0 +1,3 @@ +import * as Syntax from '@sinclair/typebox/syntax' + +console.log(Syntax) diff --git a/task/benchmark/compression/module/typebox-system.ts b/task/benchmark/compression/module/typebox-system.ts new file mode 100644 index 000000000..f148a55cf --- /dev/null +++ b/task/benchmark/compression/module/typebox-system.ts @@ -0,0 +1,3 @@ +import { TypeSystem } from '@sinclair/typebox/system' + +console.log(TypeSystem) diff --git a/task/benchmark/compression/module/typebox-value.ts b/task/benchmark/compression/module/typebox-value.ts new file mode 100644 index 000000000..a5e563352 --- /dev/null +++ b/task/benchmark/compression/module/typebox-value.ts @@ -0,0 +1,3 @@ +import { Value } from '@sinclair/typebox/value' + +console.log(Value) diff --git a/task/benchmark/compression/module/typebox.ts b/task/benchmark/compression/module/typebox.ts new file mode 100644 index 000000000..0b01566e5 --- /dev/null +++ b/task/benchmark/compression/module/typebox.ts @@ -0,0 +1,3 @@ +import { Type } from '@sinclair/typebox' + +const T = Type.String() diff --git a/task/benchmark/index.ts b/task/benchmark/index.ts new file mode 100644 index 000000000..db1450ec6 --- /dev/null +++ b/task/benchmark/index.ts @@ -0,0 +1,2 @@ +export * from './compression/index' +export * from './measurement/index' diff --git a/task/benchmark/measurement/index.ts b/task/benchmark/measurement/index.ts new file mode 100644 index 000000000..5976be49d --- /dev/null +++ b/task/benchmark/measurement/index.ts @@ -0,0 +1,5 @@ +import { shell } from '@sinclair/hammer' + +export async function measurement() { + await shell(`hammer run task/benchmark/measurement/module/index.ts --dist target/benchmark/measurement`) +} diff --git a/task/benchmark/measurement/module/benchmark.ts b/task/benchmark/measurement/module/benchmark.ts new file mode 100644 index 000000000..c4b4de381 --- /dev/null +++ b/task/benchmark/measurement/module/benchmark.ts @@ -0,0 +1,7 @@ +export namespace Benchmark { + export function Measure(execute: Function, iterations: number = 16_000_000) { + const start = Date.now() + for (let i = 0; i < iterations; i++) execute() + return { iterations, completed: Date.now() - start } + } +} diff --git a/task/benchmark/measurement/module/cases.ts b/task/benchmark/measurement/module/cases.ts new file mode 100644 index 000000000..fe2ff6a20 --- /dev/null +++ b/task/benchmark/measurement/module/cases.ts @@ -0,0 +1,146 @@ +import { Type } from '@sinclair/typebox' + +export namespace Cases { + export const Literal_String = Type.Literal('hello') + + export const Literal_Number = Type.Literal(1) + + export const Literal_Boolean = Type.Literal(true) + + export const Primitive_Number = Type.Number() + + export const Primitive_String = Type.String() + + export const Primitive_String_Pattern = Type.String({ pattern: 'foo', default: 'foo' }) + + export const Primitive_Boolean = Type.Boolean() + + export const Primitive_Null = Type.Null() + + export const Object_Unconstrained = Type.Object({ + number: Type.Number(), + negNumber: Type.Number(), + maxNumber: Type.Number(), + string: Type.String(), + longString: Type.String(), + boolean: Type.Boolean(), + deeplyNested: Type.Object({ + foo: Type.String(), + num: Type.Number(), + bool: Type.Boolean(), + }), + }) + + export const Object_Constrained = Type.Object(Object_Unconstrained.properties, { + additionalProperties: false, + }) + + export const Object_Vector3 = Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }) + + export const Object_Box3D = Type.Object({ + scale: Object_Vector3, + position: Object_Vector3, + rotate: Object_Vector3, + pivot: Object_Vector3, + }) + export const Object_Recursive = Type.Recursive( + (Recursive) => + Type.Object({ + id: Type.String(), + nodes: Type.Array(Recursive), + }), + { + default: { + id: '', + nodes: [ + { + id: '', + nodes: [ + { id: '', nodes: [] }, + { id: '', nodes: [] }, + { id: '', nodes: [] }, + ], + }, + { + id: '', + nodes: [ + { id: '', nodes: [] }, + { id: '', nodes: [] }, + { id: '', nodes: [] }, + ], + }, + { + id: '', + nodes: [ + { id: '', nodes: [] }, + { id: '', nodes: [] }, + { id: '', nodes: [] }, + ], + }, + ], + }, + }, + ) + + // prettier-ignore + export const Tuple_Primitive = Type.Tuple([ + Type.String(), + Type.Number(), + Type.Boolean() + ]) + // prettier-ignore + export const Tuple_Object = Type.Tuple([ + Type.Object({ x: Type.Number(), y: Type.Number() }), + Type.Object({ a: Type.String(), b: Type.String() }) + ]) + // prettier-ignore + export const Composite_Intersect = Type.Intersect([ + Type.Object({ x: Type.Number(), y: Type.Number() }), + Type.Object({ a: Type.String(), b: Type.String() }) + ], { default: { x: 1, y: 2, a: 'a', b: 'b' } }) + + // prettier-ignore + export const Composite_Union = Type.Union([ + Type.Object({ x: Type.Number(), y: Type.Number() }), + Type.Object({ a: Type.String(), b: Type.String() }) + ], { default: { a: 'a', b: 'b' } }) + + export const Math_Vector4 = Type.Tuple([Type.Number(), Type.Number(), Type.Number(), Type.Number()]) + + export const Math_Matrix4 = Type.Array(Type.Array(Type.Number()), { + default: [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1], + ], + }) + + export const Array_Primitive_Number = Type.Array(Type.Number(), { minItems: 4 }) + + export const Array_Primitive_String = Type.Array(Type.String(), { minItems: 4 }) + + export const Array_Primitive_Boolean = Type.Array(Type.Boolean(), { minItems: 4 }) + + export const Array_Object_Unconstrained = Type.Array(Object_Unconstrained, { minItems: 4 }) + + export const Array_Object_Constrained = Type.Array(Object_Constrained, { minItems: 4 }) + + export const Array_Object_Recursive = Type.Array(Object_Recursive, { minItems: 4 }) + + export const Array_Tuple_Primitive = Type.Array(Tuple_Primitive, { minItems: 4 }) + + export const Array_Tuple_Object = Type.Array(Tuple_Object, { minItems: 4 }) + + export const Array_Composite_Intersect = Type.Array(Composite_Intersect, { minItems: 4 }) + + export const Array_Composite_Union = Type.Array(Composite_Union, { minItems: 4 }) + + export const Array_Math_Vector4 = Type.Array(Math_Vector4, { minItems: 4 }) + + export const Array_Math_Matrix4 = Type.Array(Math_Matrix4, { minItems: 4 }) +} diff --git a/task/benchmark/measurement/module/check.ts b/task/benchmark/measurement/module/check.ts new file mode 100644 index 000000000..99d3ced51 --- /dev/null +++ b/task/benchmark/measurement/module/check.ts @@ -0,0 +1,41 @@ +import { Cases } from './cases' +import { Benchmark } from './benchmark' +import { TypeCompiler } from '@sinclair/typebox/compiler' +import { TSchema, TypeGuard } from '@sinclair/typebox' +import { Value } from '@sinclair/typebox/value' + +import Ajv from 'ajv' + +const ajv = new Ajv() // ensure single instance + +export namespace CheckBenchmark { + function Measure(type: string, schema: T) { + console.log('CheckBenchmark.Measure(', type, ')') + + const iterations = 1_000_000 + const V = Value.Create(schema) + + const AC = ajv.compile(schema) + const A = Benchmark.Measure(() => { + if (!AC(V)) throw Error() + }, iterations) + + const CC = TypeCompiler.Compile(schema) + const T = Benchmark.Measure(() => { + if (!CC.Check(V)) throw Error() + }, iterations) + + const VC = Benchmark.Measure(() => { + if (!Value.Check(schema, V)) throw Error() + }, iterations) + + return { type, ajv: A, compiler: T, value: VC } + } + + export function* Execute() { + for (const [type, schema] of Object.entries(Cases)) { + if (!TypeGuard.IsSchema(schema)) throw Error('Invalid TypeBox schema') + yield Measure(type, schema) + } + } +} diff --git a/task/benchmark/measurement/module/compile.ts b/task/benchmark/measurement/module/compile.ts new file mode 100644 index 000000000..ee0ade89c --- /dev/null +++ b/task/benchmark/measurement/module/compile.ts @@ -0,0 +1,35 @@ +import { Cases } from './cases' +import { Benchmark } from './benchmark' +import { TypeCompiler } from '@sinclair/typebox/compiler' +import { TSchema, TypeGuard } from '@sinclair/typebox' +import Ajv from 'ajv' + +const ajv = new Ajv() // ensure single instance + +export namespace CompileBenchmark { + function Measure(type: string, schema: T) { + const iterations = 1000 + console.log('CompileBenchmark.Measure(', type, ')') + // ------------------------------------------------------------------------------- + // Note: Ajv caches schemas by reference. To ensure we measure actual + // compilation times, we must pass a new reference via { ...schema } + // ------------------------------------------------------------------------------- + const AC = Benchmark.Measure(() => ajv.compile({ ...schema }), iterations) + const CC = Benchmark.Measure(() => TypeCompiler.Compile({ ...schema }), iterations) + return { type, ajv: AC, compiler: CC } + } + + export function* Execute() { + for (const [type, schema] of Object.entries(Cases)) { + if (!TypeGuard.IsSchema(schema)) throw Error('Invalid TypeBox schema') + // ------------------------------------------------------------------------------- + // Note: it is not possible to benchmark recursive schemas as ajv will cache and + // track duplicate $id (resulting in compile error). It is not possible to ammend + // recursive $id's without potentially biasing results, so we omit on this case. + // ------------------------------------------------------------------------------- + if (type === 'Object_Recursive' || type === 'Array_Object_Recursive') continue + + yield Measure(type, schema) + } + } +} diff --git a/task/benchmark/measurement/module/index.ts b/task/benchmark/measurement/module/index.ts new file mode 100644 index 000000000..65b47f716 --- /dev/null +++ b/task/benchmark/measurement/module/index.ts @@ -0,0 +1,36 @@ +import { CompileBenchmark } from './compile' +import { CheckBenchmark } from './check' +import { Result } from './result' + +export function present(results: Result[]) { + console.table( + results.reduce((acc, result) => { + const ratio = result.ajv.completed / result.compiler.completed + if (result.value) { + return { + ...acc, + [result.type.padEnd(26, ' ')]: { + Iterations: result.compiler.iterations, + ValueCheck: `${result.value.completed} ms`.padStart(10), + Ajv: `${result.ajv.completed} ms`.padStart(10), + TypeCompiler: `${result.compiler.completed} ms`.padStart(10), + Performance: `${ratio.toFixed(2)} x`.padStart(10, ' '), + }, + } + } else { + return { + ...acc, + [result.type.padEnd(26, ' ')]: { + Iterations: result.compiler.iterations, + Ajv: `${result.ajv.completed} ms`.padStart(10), + TypeCompiler: `${result.compiler.completed} ms`.padStart(10), + Performance: `${ratio.toFixed(2)} x`.padStart(10, ' '), + }, + } + } + }, {}), + ) +} + +present([...CompileBenchmark.Execute()]) +present([...CheckBenchmark.Execute()]) diff --git a/task/benchmark/measurement/module/result.ts b/task/benchmark/measurement/module/result.ts new file mode 100644 index 000000000..c74a1aa5d --- /dev/null +++ b/task/benchmark/measurement/module/result.ts @@ -0,0 +1,15 @@ +export type Result = { + type: string + ajv: { + iterations: number + completed: number + } + compiler: { + iterations: number + completed: number + } + value?: { + iterations: number + completed: number + } +} diff --git a/task/build/cjs/build.ts b/task/build/cjs/build.ts new file mode 100644 index 000000000..061043cea --- /dev/null +++ b/task/build/cjs/build.ts @@ -0,0 +1,38 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { removeNotices } from '../notices/remove-notices' +import { compile } from './compile' + +/** Builds the CommonJS version of this package */ +export async function build(target: string) { + console.log('building...cjs') + const buildTarget = `${target}/build/cjs` + await compile(buildTarget) + await removeNotices(buildTarget) +} diff --git a/task/build/cjs/compile.ts b/task/build/cjs/compile.ts new file mode 100644 index 000000000..75bd85f59 --- /dev/null +++ b/task/build/cjs/compile.ts @@ -0,0 +1,41 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +declare function shell(command: string): Promise + +// prettier-ignore +export async function compile(target: string) { + const options = [ + `--outDir ${target}`, + '--target ES2020', + '--module Node16', + '--moduleResolution Node16', + '--declaration', + ].join(' ') + await shell(`tsc -p ./src/tsconfig.json ${options}`) +} diff --git a/task/build/esm/build.ts b/task/build/esm/build.ts new file mode 100644 index 000000000..ff1c5db63 --- /dev/null +++ b/task/build/esm/build.ts @@ -0,0 +1,40 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { removeNotices } from '../notices/remove-notices' +import { convertToEsm } from './convert-to-esm' +import { compile } from './compile' + +/** Builds the ESM version of this package */ +export async function build(target: string) { + console.log('building...esm') + const buildTarget = `${target}/build/esm` + await compile(buildTarget) + await convertToEsm(buildTarget) + await removeNotices(buildTarget) +} diff --git a/task/build/esm/compile.ts b/task/build/esm/compile.ts new file mode 100644 index 000000000..6d88cfa27 --- /dev/null +++ b/task/build/esm/compile.ts @@ -0,0 +1,41 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +declare function shell(command: string): Promise + +// prettier-ignore +export async function compile(target: string) { + const options = [ + `--outDir ${target}`, + '--target ES2020', + '--module ESNext', + '--moduleResolution Bundler', + '--declaration', + ].join(' ') + await shell(`tsc -p ./src/tsconfig.json ${options}`) +} diff --git a/task/build/esm/convert-to-esm.ts b/task/build/esm/convert-to-esm.ts new file mode 100644 index 000000000..21df0eac6 --- /dev/null +++ b/task/build/esm/convert-to-esm.ts @@ -0,0 +1,110 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import * as Path from 'node:path' +import * as Fs from 'node:fs' + +// ------------------------------------------------------------------ +// Specifier Rewrite +// ------------------------------------------------------------------ + +// prettier-ignore +function replaceInlineImportSpecifiers(content: string): string { + const pattern = /import\((.*?)\)/g + while (true) { + const match = pattern.exec(content) + if (match === null) return content + const captured = match[1] + if(captured.includes('.mjs')) continue + const specifier = captured.slice(1, captured.length - 1) + content = content.replace(captured, `"${specifier}.mjs"`) + } +} +// prettier-ignore +function replaceExportSpecifiers(content: string): string { + const pattern = /(export|import)(.*) from ('(.*)');/g + while(true) { + const match = pattern.exec(content) + if(match === null) return content + const captured = match[3] + const specifier = captured.slice(1, captured.length - 1) + content = content.replace(captured, `'${specifier}.mjs'`) + } +} +function replaceSpecifiers(content: string): string { + const pass1 = replaceExportSpecifiers(content) + const pass2 = replaceInlineImportSpecifiers(pass1) + return pass2 +} + +// ------------------------------------------------------------------ +// ConvertToEsm +// ------------------------------------------------------------------ +// prettier-ignore +function shouldProcess(sourcePath: string) { + const extname = Path.extname(sourcePath) + return ['.js', '.ts'].includes(extname) +} +// prettier-ignore +function newExtension(extname: string) { + return ( + extname === '.js' ? '.mjs' : + extname === '.ts' ? '.mts' : + extname + ) +} +// prettier-ignore +function processFile(sourcePath: string) { + if(!shouldProcess(sourcePath)) return + const extname = Path.extname(sourcePath) + const dirname = Path.dirname(sourcePath) + const basename = Path.basename(sourcePath, extname) + const new_extname = newExtension(extname) + const sourceContent = Fs.readFileSync(sourcePath, 'utf-8') + const targetContent = replaceSpecifiers(sourceContent) + const targetPath = `${Path.join(dirname, basename)}${new_extname}` + Fs.writeFileSync(sourcePath, targetContent) + Fs.renameSync(sourcePath, targetPath) +} +// prettier-ignore +function processSourcePath(sourcePath: string) { + const stat = Fs.statSync(sourcePath) + if(stat.isDirectory()) return readDirectory(sourcePath) + if(stat.isFile()) return processFile(sourcePath) +} +// prettier-ignore +function readDirectory(sourceDirectory: string) { + for(const entry of Fs.readdirSync(sourceDirectory)) { + const sourcePath = Path.join(sourceDirectory, entry) + processSourcePath(sourcePath) + } +} +/** Converts the JavaScript and TypeScript declaration modules in the given source directory to use .mjs extensions */ +export function convertToEsm(sourceDirectory: string) { + readDirectory(sourceDirectory) +} diff --git a/task/build/index.ts b/task/build/index.ts new file mode 100644 index 000000000..2d9d295d9 --- /dev/null +++ b/task/build/index.ts @@ -0,0 +1,31 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +export * as Package from './package/build' +export * as Esm from './esm/build' +export * as Cjs from './cjs/build' diff --git a/task/build/notices/remove-notices.ts b/task/build/notices/remove-notices.ts new file mode 100644 index 000000000..3d101ae46 --- /dev/null +++ b/task/build/notices/remove-notices.ts @@ -0,0 +1,82 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import * as Path from 'node:path' +import * as Fs from 'node:fs' + +// ---------------------------------------------------------------------- +// Remove Module Level MIT Notice on Package Distribution +// +// The MIT copyright notice the unnecessarily increases the distribution +// size of the package, this code removes it. The MIT license is available +// in the package root. +// +// ---------------------------------------------------------------------- +// prettier-ignore +function escape(content: string) { + return content.split('').map((c) => `\\${c}`).join('') +} +// prettier-ignore +function removeNotice(content: string): string { + const open = escape('/*--------------------------------------------------------------------------') + const close = escape('---------------------------------------------------------------------------*/') + const critera = 'Permission is hereby granted, free of charge' + const pattern = new RegExp(`${open}[\\s\\S]*?${close}`, 'gm') + while (true) { + const match = pattern.exec(content) + if (match === null) return content.trimStart() + if (!match[0].includes(critera)) continue + content = content.replace(match[0], '') + } +} +// ------------------------------------------------------------------ +// Directory Enumeration +// ------------------------------------------------------------------ +// prettier-ignore +function processFile(sourcePath: string) { + const sourceContent = Fs.readFileSync(sourcePath, 'utf-8') + const targetContent = removeNotice(sourceContent) + Fs.writeFileSync(sourcePath, targetContent) +} +// prettier-ignore +function processSourcePath(sourcePath: string) { + const stat = Fs.statSync(sourcePath) + if(stat.isDirectory()) return readDirectory(sourcePath) + if(stat.isFile()) return processFile(sourcePath) +} +// prettier-ignore +function readDirectory(sourceDirectory: string) { + for(const entry of Fs.readdirSync(sourceDirectory)) { + const sourcePath = Path.join(sourceDirectory, entry) + processSourcePath(sourcePath) + } +} +/** Removes the MIT copyright notices from each source file in the given directory */ +export function removeNotices(sourceDirectory: string) { + readDirectory(sourceDirectory) +} diff --git a/task/build/package/build.ts b/task/build/package/build.ts new file mode 100644 index 000000000..a4b3c5880 --- /dev/null +++ b/task/build/package/build.ts @@ -0,0 +1,38 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import { createPackageJsonRedirect } from './create-package-json-redirect' +import { createPackageJson } from './create-package-json' + +/** Builds package.json and redirect directories */ +export async function build(target: string) { + console.log('building...package.json') + const submodules = ['compiler', 'errors', 'parser', 'syntax', 'system', 'type', 'value'] + await createPackageJsonRedirect(target, submodules) + await createPackageJson(target, submodules) +} diff --git a/task/build/package/create-package-json-redirect.ts b/task/build/package/create-package-json-redirect.ts new file mode 100644 index 000000000..5b17ef98a --- /dev/null +++ b/task/build/package/create-package-json-redirect.ts @@ -0,0 +1,50 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import * as Fs from 'node:fs' + +// prettier-ignore +function writeRedirect(target: string, submodule: string) { + Fs.mkdirSync(`${target}/${submodule}`, { recursive: true }) + Fs.writeFileSync(`${target}/${submodule}/package.json`,JSON.stringify({ + main: `../build/cjs/${submodule}/index.js`, + types: `../build/cjs/${submodule}/index.d.ts`, + }, null, 2)) +} +// -------------------------------------------------------------------------------------------------------------------------- +// Builds redirect directories for earlier versions of Node. Note that TypeScript will use these directories to +// resolve types when tsconfig.json is configured for `moduleResolution: 'Node16'`. This approach is referred to as +// `package-json-redirect` and enables correct type resolution in lieu of a correct end user configuration. +// +// https://github.com/andrewbranch/example-subpath-exports-ts-compat/tree/main/examples/node_modules/package-json-redirects +// -------------------------------------------------------------------------------------------------------------------------- + +// prettier-ignore +export function createPackageJsonRedirect(target: string, submodules: string[]) { + submodules.forEach((submodule) => writeRedirect(target, submodule)) +} diff --git a/task/build/package/create-package-json.ts b/task/build/package/create-package-json.ts new file mode 100644 index 000000000..863e922c5 --- /dev/null +++ b/task/build/package/create-package-json.ts @@ -0,0 +1,109 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typebox + +The MIT License (MIT) + +Copyright (c) 2017-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +import * as Fs from 'node:fs' +import * as Path from 'node:path' + +// prettier-ignore +export function createPackageJson(target: string, submodules: string[]) { + const content = JSON.stringify(resolvePackageJson(submodules), null, 2) + const targetPath = Path.join(target, 'package.json') + const targetDir = Path.dirname(targetPath) + Fs.mkdirSync(targetDir, { recursive: true }) + Fs.writeFileSync(targetPath, content, 'utf-8') +} +// prettier-ignore +function resolvePackageJson(submodules: string[]) { + return { + ...resolveMetadata(), + ...resolveExports(submodules) + } +} +// prettier-ignore +function resolveSubmoduleExports(submodule: string) { + return { + require: { + types: `./build/cjs/${submodule}/index.d.ts`, + default: `./build/cjs/${submodule}/index.js`, + }, + import: { + types: `./build/esm/${submodule}/index.d.mts`, + default: `./build/esm/${submodule}/index.mjs`, + } + } +} +// prettier-ignore +function resolveExports(submodules: string[]) { + const exports = submodules.reduce((acc, submodule) => { + return { ...acc, [`./${submodule}`]: resolveSubmoduleExports(submodule) } + }, { + // ... and root module + ".": { + "require": { + "types": "./build/cjs/index.d.ts", + "default": "./build/cjs/index.js", + + }, + "import": { + "types": "./build/esm/index.d.mts", + "default": "./build/esm/index.mjs", + } + } + }) + return { exports } +} +// prettier-ignore +function resolveMetadata() { + const packagePath = Path.join(process.cwd(), 'package.json') + const packageJson = JSON.parse(Fs.readFileSync(packagePath, 'utf-8')) + return { + name: packageJson.name, + version: packageJson.version, + description: packageJson.description, + keywords: packageJson.keywords, + author: packageJson.author, + license: packageJson.license, + repository: packageJson.repository, + // flagged by socket.dev if not present + scripts: { test: 'echo test' }, + types: "./build/cjs/index.d.ts", + main: "./build/cjs/index.js", + module: "./build/esm/index.mjs", + // disable auto bundle strategy: see https://github.com/esm-dev/esm.sh#bundling-strategy + 'esm.sh': { 'bundle': false }, + // specify modules with potential for side effects + 'sideEffects': [ + './build/esm/type/registry/format.mjs', + './build/esm/type/registry/type.mjs', + './build/esm/type/system/policy.mjs', + './build/cjs/type/registry/format.js', + './build/cjs/type/registry/type.js', + './build/cjs/type/system/policy.js' + ] + } +} diff --git a/test/runtime/assert/assert.ts b/test/runtime/assert/assert.ts new file mode 100644 index 000000000..315eec216 --- /dev/null +++ b/test/runtime/assert/assert.ts @@ -0,0 +1,57 @@ +import * as assert from 'assert' + +export namespace Assert { + export function HasProperty(value: unknown, key: K): asserts value is Record { + if (typeof value === 'object' && value !== null && key in value) return + throw new Error(`Expected value to have property '${key as string}'`) + } + export function IsTrue(value: boolean): asserts value is true { + return assert.strictEqual(value, true) + } + export function IsFalse(value: boolean): asserts value is false { + return assert.strictEqual(value, false) + } + export function IsEqual(actual: unknown, expect: unknown) { + if (actual instanceof Uint8Array && expect instanceof Uint8Array) { + assert.equal(actual.length, expect.length) + for (let i = 0; i < actual.length; i++) assert.equal(actual[i], expect[i]) + } + return assert.deepStrictEqual(actual, expect) + } + export function NotEqual(actual: unknown, expect: unknown) { + return assert.notEqual(actual, expect) + } + /** Asserts a numeric value is within range of the expected */ + export function InRange(value: number, expect: number, range: number) { + if (Math.abs(value - expect) <= range) return + throw Error('Expected value to be in range') + } + let nextIdOrdinal = 0 + export function NextId() { + return `$id-${nextIdOrdinal++}` + } + export function Throws(callback: Function) { + try { + callback() + } catch { + return + } + throw Error('Expected throw') + } + export async function ThrowsAsync(callback: Function) { + try { + await callback() + } catch { + return + } + throw Error('Expected throw') + } + export function IsInstanceOf any>(value: any, constructor: T): asserts value is InstanceType { + if (value instanceof constructor) return + throw Error(`Value is not instance of ${constructor}`) + } + export function IsTypeOf(value: any, type: T) { + if (typeof value === type) return + throw Error(`Value is not typeof ${type}`) + } +} diff --git a/test/runtime/assert/index.ts b/test/runtime/assert/index.ts new file mode 100644 index 000000000..c3bd55138 --- /dev/null +++ b/test/runtime/assert/index.ts @@ -0,0 +1 @@ +export * from './assert' diff --git a/test/runtime/compiler-ajv/any.ts b/test/runtime/compiler-ajv/any.ts new file mode 100644 index 000000000..695113f09 --- /dev/null +++ b/test/runtime/compiler-ajv/any.ts @@ -0,0 +1,33 @@ +import { Type } from '@sinclair/typebox' +import { Ok } from './validate' + +describe('compiler-ajv/Any', () => { + 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/test/runtime/compiler-ajv/array.ts b/test/runtime/compiler-ajv/array.ts new file mode 100644 index 000000000..589bc03ac --- /dev/null +++ b/test/runtime/compiler-ajv/array.ts @@ -0,0 +1,186 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler-ajv/Array', () => { + it('Should validate an array of any', () => { + const T = Type.Array(Type.Any()) + Ok(T, [0, true, 'hello', {}]) + }) + it('Should not validate varying array when item is number', () => { + const T = Type.Array(Type.Number()) + Fail(T, [1, 2, 3, 'hello']) + }) + 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('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('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]) + 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 composite types when passing additionalProperties false', () => { + const A = Type.Object({ a: Type.String() }) + const B = Type.Object({ b: Type.String() }) + const C = Type.Composite([A, B], { additionalProperties: 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'], + ]) + }) + it('Should not validate array with failed minItems', () => { + const T = Type.Array(Type.Number(), { minItems: 3 }) + Fail(T, [0, 1]) + }) + it('Should not validate array with failed maxItems', () => { + const T = Type.Array(Type.Number(), { maxItems: 3 }) + Fail(T, [0, 1, 2, 3]) + }) + // --------------------------------------------------------- + // Unique Items + // --------------------------------------------------------- + it('Should validate array with uniqueItems when items are distinct objects', () => { + const T = Type.Array(Type.Object({ x: Type.Number(), y: Type.Number() }), { uniqueItems: true }) + Ok(T, [ + { x: 0, y: 1 }, + { x: 1, y: 0 }, + ]) + }) + it('Should not validate array with uniqueItems when items are not distinct objects', () => { + const T = Type.Array(Type.Object({ x: Type.Number(), y: Type.Number() }), { uniqueItems: true }) + Fail(T, [ + { x: 1, y: 0 }, + { x: 1, y: 0 }, + ]) + }) + it('Should not validate array with non uniqueItems', () => { + const T = Type.Array(Type.Number(), { uniqueItems: true }) + Fail(T, [0, 0]) + }) + // --------------------------------------------------------- + // Contains + // --------------------------------------------------------- + it('Should validate for contains', () => { + const T = Type.Array(Type.Number(), { contains: Type.Literal(1) }) + Ok(T, [1]) + Ok(T, [1, 2]) + Fail(T, []) + Fail(T, [2]) + }) + it('Should validate for minContains', () => { + const T = Type.Array(Type.Number(), { contains: Type.Literal(1), minContains: 3 }) + Ok(T, [1, 1, 1, 2]) + Ok(T, [2, 1, 1, 1, 2]) + Ok(T, [1, 1, 1]) + Fail(T, []) + Fail(T, [1, 1]) + Fail(T, [2]) + }) + it('Should validate for maxContains', () => { + const T = Type.Array(Type.Number(), { contains: Type.Literal(1), maxContains: 3 }) + Ok(T, [1, 1, 1]) + Ok(T, [1, 1]) + Ok(T, [2, 2, 2, 2, 1, 1, 1]) + Fail(T, [1, 1, 1, 1]) + }) + it('Should validate for minContains and maxContains', () => { + const T = Type.Array(Type.Number(), { contains: Type.Literal(1), minContains: 3, maxContains: 5 }) + Fail(T, [1, 1]) + Ok(T, [1, 1, 1]) + Ok(T, [1, 1, 1, 1]) + Ok(T, [1, 1, 1, 1, 1]) + Fail(T, [1, 1, 1, 1, 1, 1]) + }) + it('Should not validate minContains or maxContains when contains is unspecified', () => { + const T = Type.Array(Type.Number(), { minContains: 3, maxContains: 5 }) + Fail(T, [1, 1]) + Fail(T, [1, 1, 1]) + Fail(T, [1, 1, 1, 1]) + Fail(T, [1, 1, 1, 1, 1]) + Fail(T, [1, 1, 1, 1, 1, 1]) + }) + it('Should produce illogical schema when contains is not sub type of items', () => { + const T = Type.Array(Type.Number(), { contains: Type.String(), minContains: 3, maxContains: 5 }) + Fail(T, [1, 1]) + Fail(T, [1, 1, 1]) + Fail(T, [1, 1, 1, 1]) + Fail(T, [1, 1, 1, 1, 1]) + Fail(T, [1, 1, 1, 1, 1, 1]) + }) + // ---------------------------------------------------------------- + // Issue: https://github.com/sinclairzx81/typebox/discussions/607 + // ---------------------------------------------------------------- + it('Should correctly handle undefined array properties', () => { + const Answer = Type.Object({ + text: Type.String(), + isCorrect: Type.Boolean(), + }) + const Question = Type.Object({ + text: Type.String(), + options: Type.Array(Answer, { + minContains: 1, + maxContains: 1, + contains: Type.Object({ + text: Type.String(), + isCorrect: Type.Literal(true), + }), + }), + }) + Fail(Question, { text: 'A' }) + Fail(Question, { text: 'A', options: [] }) + Ok(Question, { text: 'A', options: [{ text: 'A', isCorrect: true }] }) + Ok(Question, { + text: 'A', + options: [ + { text: 'A', isCorrect: true }, + { text: 'B', isCorrect: false }, + ], + }) + Fail(Question, { text: 'A', options: [{ text: 'A', isCorrect: false }] }) + Fail(Question, { + text: 'A', + options: [ + { text: 'A', isCorrect: true }, + { text: 'B', isCorrect: true }, + ], + }) + }) +}) diff --git a/test/runtime/compiler-ajv/boolean.ts b/test/runtime/compiler-ajv/boolean.ts new file mode 100644 index 000000000..6ff4f7861 --- /dev/null +++ b/test/runtime/compiler-ajv/boolean.ts @@ -0,0 +1,34 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler-ajv/Boolean', () => { + 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/test/runtime/compiler-ajv/composite.ts b/test/runtime/compiler-ajv/composite.ts new file mode 100644 index 000000000..3d1d41a62 --- /dev/null +++ b/test/runtime/compiler-ajv/composite.ts @@ -0,0 +1,111 @@ +import { Type } from '@sinclair/typebox' +import { Assert } from '../assert' +import { Ok, Fail } from './validate' + +describe('compiler-ajv/Composite', () => { + it('Should compose two objects', () => { + const A = Type.Object({ a: Type.String() }) + const B = Type.Object({ b: Type.Number() }) + const T = Type.Composite([A, B], { additionalProperties: false }) + Ok(T, { a: 'hello', b: 42 }) + }) + it('Should compose with partial', () => { + const A = Type.Partial(Type.Object({ a: Type.Number() })) + const B = Type.Partial(Type.Object({ b: Type.Number() })) + const P = Type.Composite([A, B], { additionalProperties: false }) + Ok(P, { a: 1, b: 2 }) + Ok(P, { a: 1 }) + Ok(P, { b: 1 }) + Ok(P, {}) + Fail(P, { a: 1, b: 2, c: 3 }) + Fail(P, { c: 1 }) + }) + it('Should compose with overlapping same type', () => { + const A = Type.Object({ a: Type.Number() }) + const B = Type.Object({ a: Type.Number() }) + const P = Type.Composite([A, B]) + Ok(P, { a: 1 }) + Fail(P, { a: '1' }) + }) + it('Should not compose with overlapping varying type', () => { + const A = Type.Object({ a: Type.Number() }) + const B = Type.Object({ a: Type.String() }) + const T = Type.Composite([A, B]) + Fail(T, { a: 1 }) + Fail(T, { a: 'hello' }) + Fail(T, {}) + }) + it('Should compose with deeply nest overlapping varying type', () => { + const A = Type.Object({ a: Type.Number() }) + const B = Type.Object({ b: Type.String() }) + const C = Type.Object({ c: Type.Boolean() }) + const D = Type.Object({ d: Type.Null() }) + const T = Type.Composite([A, B, C, D]) + Ok(T, { a: 1, b: 'hello', c: true, d: null }) + }) + it('Should not compose with deeply nest overlapping varying type', () => { + const A = Type.Object({ a: Type.Number() }) + const B = Type.Object({ a: Type.String() }) + const C = Type.Object({ a: Type.Boolean() }) + const D = Type.Object({ a: Type.Null() }) + const T = Type.Composite([A, B, C, D]) + Fail(T, { a: 1 }) + Fail(T, { a: 'hello' }) + Fail(T, { a: false }) + Fail(T, { a: null }) + Fail(T, { a: [] }) + Fail(T, {}) + }) + it('Should pick from composited type', () => { + const A = Type.Object({ x: Type.Number() }) + const B = Type.Object({ y: Type.Number() }) + const C = Type.Object({ z: Type.Number() }) + const T = Type.Composite([A, B, C]) + const P = Type.Pick(T, ['x', 'y'], { additionalProperties: false }) + Ok(P, { x: 1, y: 1 }) + Fail(P, { x: 1, y: 1, z: 1 }) + }) + it('Should omit from composited type', () => { + const A = Type.Object({ x: Type.Number() }) + const B = Type.Object({ y: Type.Number() }) + const C = Type.Object({ z: Type.Number() }) + const T = Type.Composite([A, B, C]) + const P = Type.Omit(T, ['z'], { additionalProperties: false }) + Ok(P, { x: 1, y: 1 }) + Fail(P, { x: 1, y: 1, z: 1 }) + }) + it('Should compose nested object properties', () => { + const A = Type.Object({ x: Type.Object({ x: Type.Number() }) }) + const B = Type.Object({ y: Type.Object({ x: Type.String() }) }) + const T = Type.Composite([A, B]) + Ok(T, { x: { x: 1 }, y: { x: '' } }) + Fail(T, { x: { x: '1' }, y: { x: '' } }) + Fail(T, { x: { x: 1 }, y: { x: 1 } }) + }) + // todo: move to composition / type guard spec + it('Should compose and produce the same schema', () => { + const T = Type.Object({ + field: Type.Optional(Type.String()), + }) + const A = Type.Composite([T]) + const B = Type.Composite([T]) + Assert.IsEqual(A, B) + }) + // prettier-ignore + it('Should composite intersection', () => { + const T = Type.Composite([ + Type.Intersect([ + Type.Object({ x: Type.Number() }) + ]), + Type.Intersect([ + Type.Object({ y: Type.Number() }) + ]), + Type.Intersect([ + Type.Object({ z: Type.Number() }) + ]), + ]) + Ok(T, { x: 1, y: 2, z: 3 }) + Fail(T, { x: 1, y: 2, z: '3' }) + Fail(T, { x: 1, y: 2 }) + }) +}) diff --git a/test/runtime/compiler-ajv/const.ts b/test/runtime/compiler-ajv/const.ts new file mode 100644 index 000000000..21705e276 --- /dev/null +++ b/test/runtime/compiler-ajv/const.ts @@ -0,0 +1,39 @@ +import { Type } from '@sinclair/typebox' +import { Ok } from './validate' + +describe('compiler-ajv/Const', () => { + it('Should validate 1', () => { + const T = Type.Const(1) + Ok(T, 1) + }) + it('Should validate 2', () => { + const T = Type.Const('hello') + Ok(T, 'hello') + }) + it('Should validate 3', () => { + const T = Type.Const(true) + Ok(T, true) + }) + it('Should validate 4', () => { + const T = Type.Const({ x: 1, y: 2 }) + Ok(T, { x: 1, y: 2 }) + }) + it('Should validate 5', () => { + const T = Type.Const([1, 2, 3]) + Ok(T, [1, 2, 3]) + }) + it('Should validate 6', () => { + const T = Type.Const([1, true, 'hello']) + Ok(T, [1, true, 'hello']) + }) + it('Should validate 7', () => { + const T = Type.Const({ + x: [1, 2, 3, 4], + y: { x: 1, y: 2, z: 3 }, + }) + Ok(T, { + x: [1, 2, 3, 4], + y: { x: 1, y: 2, z: 3 }, + }) + }) +}) diff --git a/test/runtime/compiler-ajv/date.ts b/test/runtime/compiler-ajv/date.ts new file mode 100644 index 000000000..f5f8e12fc --- /dev/null +++ b/test/runtime/compiler-ajv/date.ts @@ -0,0 +1,70 @@ +// --------------------------------------------------- +// No Longer Supported +// --------------------------------------------------- + +// import { Type } from '@sinclair/typebox' +// import { Ok, Fail } from './validate' + +// ---------------------------------------------------- +// These tests are implemented by way of .addKeyword() +// which are configured to use Value.Check() +// ---------------------------------------------------- + +// describe('compiler-ajv/Date', () => { +// it('Should not validate number', () => { +// const T = Type.Date() +// Fail(T, 1) +// }) +// it('Should not validate string', () => { +// const T = Type.Date() +// Fail(T, 'hello') +// }) +// it('Should not validate boolean', () => { +// const T = Type.Date() +// Fail(T, true) +// }) +// it('Should not validate array', () => { +// const T = Type.Date() +// Fail(T, [1, 2, 3]) +// }) +// it('Should not validate object', () => { +// const T = Type.Date() +// Fail(T, { a: 1, b: 2 }) +// }) +// it('Should not validate null', () => { +// const T = Type.Date() +// Fail(T, null) +// }) +// it('Should not validate undefined', () => { +// const T = Type.Date() +// Fail(T, undefined) +// }) +// it('Should validate Date', () => { +// const T = Type.Date() +// Ok(T, new Date()) +// }) +// it('Should not validate Date if is invalid', () => { +// const T = Type.Date() +// Fail(T, new Date('not-a-valid-date')) +// }) +// it('Should validate Date minimumTimestamp', () => { +// const T = Type.Date({ minimumTimestamp: 10 }) +// Fail(T, new Date(9)) +// Ok(T, new Date(10)) +// }) +// it('Should validate Date maximumTimestamp', () => { +// const T = Type.Date({ maximumTimestamp: 10 }) +// Ok(T, new Date(10)) +// Fail(T, new Date(11)) +// }) +// it('Should validate Date exclusiveMinimumTimestamp', () => { +// const T = Type.Date({ exclusiveMinimumTimestamp: 10 }) +// Fail(T, new Date(10)) +// Ok(T, new Date(11)) +// }) +// it('Should validate Date exclusiveMaximumTimestamp', () => { +// const T = Type.Date({ exclusiveMaximumTimestamp: 10 }) +// Ok(T, new Date(9)) +// Fail(T, new Date(10)) +// }) +// }) diff --git a/test/runtime/compiler-ajv/enum.ts b/test/runtime/compiler-ajv/enum.ts new file mode 100644 index 000000000..c4599be98 --- /dev/null +++ b/test/runtime/compiler-ajv/enum.ts @@ -0,0 +1,53 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler-ajv/Enum', () => { + 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) + }) + 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') + }) + 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') + }) + 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) + }) + 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) + }) +}) diff --git a/spec/types/typebox.test-d.ts b/test/runtime/compiler-ajv/index.ts similarity index 59% rename from spec/types/typebox.test-d.ts rename to test/runtime/compiler-ajv/index.ts index 519bba3d5..3963d5c22 100644 --- a/spec/types/typebox.test-d.ts +++ b/test/runtime/compiler-ajv/index.ts @@ -1,12 +1,17 @@ import './any' import './array' import './boolean' -import './emum' +import './composite' +import './const' +import './date' +import './enum' +import './integer' import './intersect' import './keyof' import './literal' -import './modifier' -import './namespace' +import './module' +import './never' +import './not' import './null' import './number' import './object' @@ -16,14 +21,16 @@ import './partial' import './pick' import './readonly-optional' import './readonly' -import './rec' +import './recursive' import './record' import './ref' -import './regex' import './required' -import './strict' +import './string-pattern' import './string' +import './template-literal' import './tuple' +import './uint8array' import './union' import './unknown' - +import './unsafe' +import './void' diff --git a/test/runtime/compiler-ajv/integer.ts b/test/runtime/compiler-ajv/integer.ts new file mode 100644 index 000000000..664eacf7b --- /dev/null +++ b/test/runtime/compiler-ajv/integer.ts @@ -0,0 +1,60 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler-ajv/Integer', () => { + it('Should not validate number', () => { + const T = Type.Integer() + Fail(T, 3.14) + }) + it('Should validate integer', () => { + const T = Type.Integer() + Ok(T, 1) + }) + it('Should not validate string', () => { + const T = Type.Integer() + Fail(T, 'hello') + }) + it('Should not validate boolean', () => { + const T = Type.Integer() + Fail(T, true) + }) + it('Should not validate array', () => { + const T = Type.Integer() + Fail(T, [1, 2, 3]) + }) + it('Should not validate object', () => { + const T = Type.Integer() + Fail(T, { a: 1, b: 2 }) + }) + it('Should not validate null', () => { + const T = Type.Integer() + Fail(T, null) + }) + it('Should not validate undefined', () => { + const T = Type.Integer() + Fail(T, undefined) + }) + it('Should validate minimum', () => { + const T = Type.Integer({ minimum: 10 }) + Fail(T, 9) + Ok(T, 10) + }) + it('Should validate maximum', () => { + const T = Type.Integer({ maximum: 10 }) + Ok(T, 10) + Fail(T, 11) + }) + it('Should validate Date exclusiveMinimum', () => { + const T = Type.Integer({ exclusiveMinimum: 10 }) + Fail(T, 10) + Ok(T, 11) + }) + it('Should validate Date exclusiveMaximum', () => { + const T = Type.Integer({ exclusiveMaximum: 10 }) + Ok(T, 9) + Fail(T, 10) + }) + it('Should not validate NaN', () => { + Fail(Type.Integer(), NaN) + }) +}) diff --git a/test/runtime/compiler-ajv/intersect.ts b/test/runtime/compiler-ajv/intersect.ts new file mode 100644 index 000000000..00fe0ac1a --- /dev/null +++ b/test/runtime/compiler-ajv/intersect.ts @@ -0,0 +1,218 @@ +import { Type, Static } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler-ajv/Intersect', () => { + it('Should intersect number and number', () => { + const A = Type.Number() + const B = Type.Number() + const T = Type.Intersect([A, B], {}) + Ok(T, 1) + }) + it('Should not intersect string and number', () => { + const A = Type.String() + const B = Type.Number() + const T = Type.Intersect([A, B], {}) + Fail(T, 1) + Fail(T, '1') + }) + it('Should intersect two objects', () => { + const A = Type.Object({ x: Type.Number() }) + const B = Type.Object({ y: Type.Number() }) + const T = Type.Intersect([A, B], {}) + Ok(T, { x: 1, y: 1 }) + }) + it('Should not intersect two objects with internal additionalProperties false', () => { + const A = Type.Object({ x: Type.Number() }, { additionalProperties: false }) + const B = Type.Object({ y: Type.Number() }, { additionalProperties: false }) + const T = Type.Intersect([A, B], {}) + Fail(T, { x: 1, y: 1 }) + }) + it('Should intersect two objects and mandate required properties', () => { + const A = Type.Object({ x: Type.Number() }) + const B = Type.Object({ y: Type.Number() }) + const T = Type.Intersect([A, B], {}) + Ok(T, { x: 1, y: 1 }) + Fail(T, { x: 1 }) + Fail(T, { y: 1 }) + }) + it('Should intersect two objects with unevaluated properties', () => { + const A = Type.Object({ x: Type.Number() }) + const B = Type.Object({ y: Type.Number() }) + const T = Type.Intersect([A, B], {}) + Ok(T, { x: 1, y: 2, z: 1 }) + }) + it('Should intersect two objects and restrict unevaluated properties', () => { + const A = Type.Object({ x: Type.Number() }) + const B = Type.Object({ y: Type.Number() }) + const T = Type.Intersect([A, B], { unevaluatedProperties: false }) + Fail(T, { x: 1, y: 2, z: 1 }) + }) + it('Should intersect two objects and allow unevaluated properties of number', () => { + const A = Type.Object({ x: Type.Number() }) + const B = Type.Object({ y: Type.Number() }) + const T = Type.Intersect([A, B], { unevaluatedProperties: Type.Number() }) + Ok(T, { x: 1, y: 2, z: 3 }) + Fail(T, { x: 1, y: 2, z: '1' }) + }) + it('Should intersect two union objects with overlapping properties of the same type', () => { + const A = Type.Union([Type.Object({ x: Type.Number() })]) + const B = Type.Union([Type.Object({ x: Type.Number() })]) + const T = Type.Intersect([A, B]) + Ok(T, { x: 1 }) + Fail(T, { x: '1' }) + }) + it('Should not intersect two union objects with overlapping properties of varying types', () => { + const A = Type.Union([Type.Object({ x: Type.Number() })]) + const B = Type.Union([Type.Object({ x: Type.String() })]) + const T = Type.Intersect([A, B]) + Fail(T, { x: 1 }) + Fail(T, { x: '1' }) + }) + it('Should intersect two union objects with non-overlapping properties', () => { + const A = Type.Union([Type.Object({ x: Type.Number() })]) + const B = Type.Union([Type.Object({ y: Type.Number() })]) + const T = Type.Intersect([A, B]) + Ok(T, { x: 1, y: 1 }) + }) + it('Should not intersect two union objects with non-overlapping properties for additionalProperties false', () => { + const A = Type.Union([Type.Object({ x: Type.Number() }, { additionalProperties: false })]) + const B = Type.Union([Type.Object({ y: Type.Number() }, { additionalProperties: false })]) + const T = Type.Intersect([A, B]) + Fail(T, { x: 1, y: 1 }) + }) + it('unevaluatedProperties with Record 1', () => { + const T = Type.Intersect( + [ + Type.Record(Type.Number(), Type.String()), + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + ], + { + unevaluatedProperties: false, + }, + ) + Ok(T, { x: 1, y: 2 }) + }) + it('unevaluatedProperties with Record 2', () => { + const T = Type.Intersect( + [ + Type.Record(Type.Number(), Type.String()), + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + ], + { + unevaluatedProperties: false, + }, + ) + Ok(T, { x: 1, y: 2, 0: 'hello' }) + }) + it('unevaluatedProperties with Record 3', () => { + const T = Type.Intersect( + [ + Type.Record(Type.Number(), Type.String()), + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + ], + { + unevaluatedProperties: false, + }, + ) + Fail(T, { x: 1, y: 2, 0: 1 }) + }) + it('unevaluatedProperties with Record 4', () => { + const T = Type.Intersect( + [ + Type.Record(Type.Number(), Type.String()), + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + ], + { + unevaluatedProperties: Type.Boolean(), + }, + ) + Ok(T, { x: 1, y: 2 }) + }) + it('unevaluatedProperties with Record 5', () => { + const T = Type.Intersect( + [ + Type.Record(Type.Number(), Type.String()), + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + ], + { + unevaluatedProperties: Type.Boolean(), + }, + ) + Ok(T, { x: 1, y: 2, z: true }) + }) + it('unevaluatedProperties with Record 6', () => { + const T = Type.Intersect( + [ + Type.Record(Type.Number(), Type.String()), + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + ], + { + unevaluatedProperties: Type.Boolean(), + }, + ) + Fail(T, { x: 1, y: 2, z: 1 }) + }) + it('unevaluatedProperties with Record 7', () => { + const T = Type.Intersect( + [ + Type.Record(Type.Number(), Type.String()), + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + ], + { + unevaluatedProperties: Type.Boolean(), + }, + ) + Ok(T, { x: 1, y: 2, 0: '' }) + }) + it('unevaluatedProperties with Record 8', () => { + const T = Type.Intersect( + [ + Type.Record(Type.Number(), Type.String()), + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + ], + { + unevaluatedProperties: Type.Boolean(), + }, + ) + Ok(T, { x: 1, y: 2, 0: '', z: true }) + }) + it('unevaluatedProperties with Record 9', () => { + const T = Type.Intersect( + [ + Type.Record(Type.Number(), Type.String()), + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + ], + { + unevaluatedProperties: Type.Boolean(), + }, + ) + Fail(T, { x: 1, y: 2, 0: '', z: 1 }) + }) +}) diff --git a/test/runtime/compiler-ajv/keyof.ts b/test/runtime/compiler-ajv/keyof.ts new file mode 100644 index 000000000..0734a2f93 --- /dev/null +++ b/test/runtime/compiler-ajv/keyof.ts @@ -0,0 +1,48 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler-ajv/KeyOf', () => { + 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(), + }), + ) + Ok(T, 'x') + Ok(T, 'y') + Ok(T, 'z') + Fail(T, 'w') + }) + it('Should validate when using pick', () => { + const T = Type.KeyOf( + Type.Pick( + Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }), + ['x', 'y'], + ), + ) + Ok(T, 'x') + Ok(T, 'y') + Fail(T, 'z') + }) + it('Should validate when using omit', () => { + const T = Type.KeyOf( + Type.Omit( + Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }), + ['x', 'y'], + ), + ) + Fail(T, 'x') + Fail(T, 'y') + Ok(T, 'z') + }) +}) diff --git a/test/runtime/compiler-ajv/literal.ts b/test/runtime/compiler-ajv/literal.ts new file mode 100644 index 000000000..a41d69044 --- /dev/null +++ b/test/runtime/compiler-ajv/literal.ts @@ -0,0 +1,50 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler-ajv/Literal', () => { + 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') + }) + 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') + }) + // reference: https://github.com/sinclairzx81/typebox/issues/539 + it('Should escape single quote literals', () => { + const T = Type.Literal("it's") + Ok(T, "it's") + Fail(T, "it''s") + }) + it('Should escape multiple single quote literals', () => { + const T = Type.Literal("'''''''''") + Ok(T, "'''''''''") + Fail(T, "''''''''") // minus 1 + }) +}) diff --git a/test/runtime/compiler-ajv/module.ts b/test/runtime/compiler-ajv/module.ts new file mode 100644 index 000000000..0797dd451 --- /dev/null +++ b/test/runtime/compiler-ajv/module.ts @@ -0,0 +1,144 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler-ajv/Module', () => { + it('Should validate string', () => { + const Module = Type.Module({ + A: Type.String(), + }) + const T = Module.Import('A') + Ok(T, 'hello') + Fail(T, true) + }) + it('Should validate referenced string', () => { + const Module = Type.Module({ + A: Type.String(), + B: Type.Ref('A'), + }) + const T = Module.Import('B') + Ok(T, 'hello') + Fail(T, true) + }) + it('Should validate self referential', () => { + const Module = Type.Module({ + A: Type.Object({ + nodes: Type.Array(Type.Ref('A')), + }), + }) + const T = Module.Import('A') + Ok(T, { nodes: [{ nodes: [{ nodes: [] }, { nodes: [] }] }] }) + Fail(T, { nodes: [{ nodes: [{ nodes: [] }, { nodes: false }] }] }) + Fail(T, true) + }) + it('Should validate mutual recursive', () => { + const Module = Type.Module({ + A: Type.Object({ + b: Type.Ref('B'), + }), + B: Type.Object({ + a: Type.Union([Type.Ref('A'), Type.Null()]), + }), + }) + const T = Module.Import('A') + Ok(T, { b: { a: null } }) + Ok(T, { b: { a: { b: { a: null } } } }) + Fail(T, { b: { a: 1 } }) + Fail(T, { b: { a: { b: { a: 1 } } } }) + Fail(T, true) + }) + it('Should validate mutual recursive (Array)', () => { + const Module = Type.Module({ + A: Type.Object({ + b: Type.Ref('B'), + }), + B: Type.Object({ + a: Type.Array(Type.Ref('A')), + }), + }) + const T = Module.Import('A') + Ok(T, { b: { a: [{ b: { a: [] } }] } }) + Fail(T, { b: { a: [{ b: { a: [null] } }] } }) + Fail(T, true) + }) + it('Should validate deep referential', () => { + const Module = Type.Module({ + A: Type.Ref('B'), + B: Type.Ref('C'), + C: Type.Ref('D'), + D: Type.Ref('E'), + E: Type.Ref('F'), + F: Type.Ref('G'), + G: Type.Ref('H'), + H: Type.Literal('hello'), + }) + const T = Module.Import('A') + Ok(T, 'hello') + Fail(T, 'world') + }) + // ---------------------------------------------------------------- + // Modifiers + // ---------------------------------------------------------------- + it('Should validate objects with property modifiers 1', () => { + const Module = Type.Module({ + T: Type.Object({ + x: Type.ReadonlyOptional(Type.Null()), + y: Type.Readonly(Type.Null()), + z: Type.Optional(Type.Null()), + w: Type.Null(), + }), + }) + const T = Module.Import('T') + Ok(T, { x: null, y: null, w: null }) + Ok(T, { y: null, w: null }) + Fail(T, { x: 1, y: null, w: null }) + }) + it('Should validate objects with property modifiers 2', () => { + const Module = Type.Module({ + T: Type.Object({ + x: Type.ReadonlyOptional(Type.Array(Type.Null())), + y: Type.Readonly(Type.Array(Type.Null())), + z: Type.Optional(Type.Array(Type.Null())), + w: Type.Array(Type.Null()), + }), + }) + const T = Module.Import('T') + Ok(T, { x: [null], y: [null], w: [null] }) + Ok(T, { y: [null], w: [null] }) + Fail(T, { x: [1], y: [null], w: [null] }) + }) + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/1109 + // ---------------------------------------------------------------- + it('Should validate deep referential 1', () => { + const Module = Type.Module({ + A: Type.Union([Type.Literal('Foo'), Type.Literal('Bar')]), + B: Type.Ref('A'), + C: Type.Object({ ref: Type.Ref('B') }), + D: Type.Union([Type.Ref('B'), Type.Ref('C')]), + }) + Ok(Module.Import('A') as never, 'Foo') + Ok(Module.Import('A') as never, 'Bar') + Ok(Module.Import('B') as never, 'Foo') + Ok(Module.Import('B') as never, 'Bar') + Ok(Module.Import('C') as never, { ref: 'Foo' }) + Ok(Module.Import('C') as never, { ref: 'Bar' }) + Ok(Module.Import('D') as never, 'Foo') + Ok(Module.Import('D') as never, 'Bar') + Ok(Module.Import('D') as never, { ref: 'Foo' }) + Ok(Module.Import('D') as never, { ref: 'Bar' }) + }) + it('Should validate deep referential 2', () => { + const Module = Type.Module({ + A: Type.Literal('Foo'), + B: Type.Ref('A'), + C: Type.Ref('B'), + D: Type.Ref('C'), + E: Type.Ref('D'), + }) + Ok(Module.Import('A'), 'Foo') + Ok(Module.Import('B'), 'Foo') + Ok(Module.Import('C'), 'Foo') + Ok(Module.Import('D'), 'Foo') + Ok(Module.Import('E'), 'Foo') + }) +}) diff --git a/test/runtime/compiler-ajv/never.ts b/test/runtime/compiler-ajv/never.ts new file mode 100644 index 000000000..49904f4eb --- /dev/null +++ b/test/runtime/compiler-ajv/never.ts @@ -0,0 +1,33 @@ +import { Type } from '@sinclair/typebox' +import { Fail } from './validate' + +describe('compiler-ajv/Never', () => { + it('Should not validate number', () => { + const T = Type.Never() + Fail(T, 1) + }) + it('Should not validate string', () => { + const T = Type.Never() + Fail(T, 'hello') + }) + it('Should not validate boolean', () => { + const T = Type.Never() + Fail(T, true) + }) + it('Should not validate array', () => { + const T = Type.Never() + Fail(T, [1, 2, 3]) + }) + it('Should not validate object', () => { + const T = Type.Never() + Fail(T, { a: 1, b: 2 }) + }) + it('Should not validate null', () => { + const T = Type.Never() + Fail(T, null) + }) + it('Should validate undefined', () => { + const T = Type.Never() + Fail(T, undefined) + }) +}) diff --git a/test/runtime/compiler-ajv/not.ts b/test/runtime/compiler-ajv/not.ts new file mode 100644 index 000000000..013cf28a5 --- /dev/null +++ b/test/runtime/compiler-ajv/not.ts @@ -0,0 +1,45 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler-ajv/Not', () => { + it('Should validate not number', () => { + const T = Type.Not(Type.Number()) + Fail(T, 1) + Ok(T, '1') + }) + it('Should validate not not number', () => { + const T = Type.Not(Type.Not(Type.Number())) + Ok(T, 1) + Fail(T, '1') + }) + it('Should validate not union', () => { + // prettier-ignore + const T = Type.Not(Type.Union([ + Type.Literal('A'), + Type.Literal('B'), + Type.Literal('C') + ])) + Fail(T, 'A') + Fail(T, 'B') + Fail(T, 'C') + Ok(T, 'D') + }) + it('Should validate not object intersection', () => { + const T = Type.Intersect([ + Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }), + Type.Object({ + x: Type.Not(Type.Literal(0)), + y: Type.Not(Type.Literal(0)), + z: Type.Not(Type.Literal(0)), + }), + ]) + Fail(T, { x: 0, y: 0, z: 0 }) + Fail(T, { x: 1, y: 0, z: 0 }) + Fail(T, { x: 1, y: 1, z: 0 }) + Ok(T, { x: 1, y: 1, z: 1 }) + }) +}) diff --git a/test/runtime/compiler-ajv/null.ts b/test/runtime/compiler-ajv/null.ts new file mode 100644 index 000000000..4947b0cf9 --- /dev/null +++ b/test/runtime/compiler-ajv/null.ts @@ -0,0 +1,33 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler-ajv/Null', () => { + 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/test/runtime/compiler-ajv/number.ts b/test/runtime/compiler-ajv/number.ts new file mode 100644 index 000000000..30dd7fb8a --- /dev/null +++ b/test/runtime/compiler-ajv/number.ts @@ -0,0 +1,60 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler-ajv/Number', () => { + it('Should validate number', () => { + const T = Type.Number() + Ok(T, 3.14) + }) + it('Should validate integer', () => { + 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) + }) + it('Should validate minimum', () => { + const T = Type.Number({ minimum: 10 }) + Fail(T, 9) + Ok(T, 10) + }) + it('Should validate maximum', () => { + const T = Type.Number({ maximum: 10 }) + Ok(T, 10) + Fail(T, 11) + }) + it('Should validate Date exclusiveMinimum', () => { + const T = Type.Number({ exclusiveMinimum: 10 }) + Fail(T, 10) + Ok(T, 11) + }) + it('Should validate Date exclusiveMaximum', () => { + const T = Type.Number({ exclusiveMaximum: 10 }) + Ok(T, 9) + Fail(T, 10) + }) + it('Should not validate NaN', () => { + Fail(Type.Number(), NaN) + }) +}) diff --git a/test/runtime/compiler-ajv/object.ts b/test/runtime/compiler-ajv/object.ts new file mode 100644 index 000000000..c096bc546 --- /dev/null +++ b/test/runtime/compiler-ajv/object.ts @@ -0,0 +1,204 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler-ajv/Object', () => { + it('Should not validate a number', () => { + const T = Type.Object({}) + Fail(T, 42) + }) + it('Should not validate a string', () => { + const T = Type.Object({}) + Fail(T, 'hello') + }) + it('Should not validate a boolean', () => { + const T = Type.Object({}) + Fail(T, true) + }) + it('Should not validate a null', () => { + const T = Type.Object({}) + Fail(T, null) + }) + it('Should not validate an array', () => { + const T = Type.Object({}) + Fail(T, [1, 2]) + }) + 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 }, + }) + }) + + it('Should allow additionalProperties by default', () => { + const T = Type.Object({ + a: Type.Number(), + b: Type.String(), + }) + Ok(T, { + a: 1, + b: 'hello', + c: true, + }) + }) + + it('Should not allow an empty object if minProperties is set to 1', () => { + const T = Type.Object( + { + a: Type.Optional(Type.Number()), + b: Type.Optional(Type.String()), + }, + { additionalProperties: false, minProperties: 1 }, + ) + Ok(T, { a: 1 }) + Ok(T, { b: 'hello' }) + Fail(T, {}) + }) + + it('Should not allow 3 properties if maxProperties is set to 2', () => { + const T = Type.Object( + { + a: Type.Optional(Type.Number()), + b: Type.Optional(Type.String()), + c: Type.Optional(Type.Boolean()), + }, + { additionalProperties: false, maxProperties: 2 }, + ) + Ok(T, { a: 1 }) + Ok(T, { a: 1, b: 'hello' }) + Fail(T, { + a: 1, + b: 'hello', + c: true, + }) + }) + + 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, + }) + }) + + it('Should not allow properties for an empty object when additionalProperties is false', () => { + const T = Type.Object({}, { additionalProperties: false }) + Ok(T, {}) + Fail(T, { a: 10 }) + }) + + it('Should validate with non-syntax property keys', () => { + const T = Type.Object({ + 'with-hyphen': Type.Literal(1), + '0-leading': Type.Literal(2), + '$-leading': Type.Literal(3), + '!@#$%^&*(': Type.Literal(4), + }) + Ok(T, { + 'with-hyphen': 1, + '0-leading': 2, + '$-leading': 3, + '!@#$%^&*(': 4, + }) + }) + + it('Should validate schema additional properties of string', () => { + const T = Type.Object( + { + x: Type.Number(), + y: Type.Number(), + }, + { + additionalProperties: Type.String(), + }, + ) + Ok(T, { + x: 1, + y: 2, + z: 'hello', + }) + Fail(T, { + x: 1, + y: 2, + z: 3, + }) + }) + + it('Should validate schema additional properties of array', () => { + const T = Type.Object( + { + x: Type.Number(), + y: Type.Number(), + }, + { + additionalProperties: Type.Array(Type.Number()), + }, + ) + Ok(T, { + x: 1, + y: 2, + z: [0, 1, 2], + }) + Fail(T, { + x: 1, + y: 2, + z: 3, + }) + }) + + it('Should validate schema additional properties of object', () => { + const T = Type.Object( + { + x: Type.Number(), + y: Type.Number(), + }, + { + additionalProperties: Type.Object({ + z: Type.Number(), + }), + }, + ) + Ok(T, { + x: 1, + y: 2, + z: { z: 1 }, + }) + Fail(T, { + x: 1, + y: 2, + z: 3, + }) + }) +}) diff --git a/test/runtime/compiler-ajv/omit.ts b/test/runtime/compiler-ajv/omit.ts new file mode 100644 index 000000000..0053c6808 --- /dev/null +++ b/test/runtime/compiler-ajv/omit.ts @@ -0,0 +1,69 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' +import { strictEqual } from 'assert' + +describe('compiler-ajv/Omit', () => { + it('Should omit properties on the source schema', () => { + const A = Type.Object( + { + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }, + { 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 delete the required property if no required properties remain', () => { + const A = Type.Object( + { + x: Type.Optional(Type.Number()), + y: Type.Readonly(Type.Optional(Type.Number())), + z: Type.Number(), + }, + { additionalProperties: false }, + ) + const T = Type.Omit(A, ['z']) + strictEqual(T.required, undefined) + }) + 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) + }) + it('Should omit with keyof object', () => { + const A = Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }) + const B = Type.Object({ + x: Type.Number(), + y: Type.Number(), + }) + const T = Type.Omit(A, Type.KeyOf(B), { additionalProperties: false }) + Ok(T, { z: 0 }) + Fail(T, { x: 0, y: 0, z: 0 }) + }) +}) diff --git a/test/runtime/compiler-ajv/optional.ts b/test/runtime/compiler-ajv/optional.ts new file mode 100644 index 000000000..a5d39dd6f --- /dev/null +++ b/test/runtime/compiler-ajv/optional.ts @@ -0,0 +1,27 @@ +import { strictEqual } from 'assert' +import { Type } from '@sinclair/typebox' +import { Ok } from './validate' + +describe('compiler-ajv/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/test/runtime/compiler-ajv/partial.ts b/test/runtime/compiler-ajv/partial.ts new file mode 100644 index 000000000..5be5691a3 --- /dev/null +++ b/test/runtime/compiler-ajv/partial.ts @@ -0,0 +1,52 @@ +import { Type, ReadonlyKind, OptionalKind } from '@sinclair/typebox' +import { Ok } from './validate' +import { Assert } from '../assert' + +describe('compiler-ajv/Partial', () => { + it('Should convert a required object into a partial.', () => { + const A = Type.Object( + { + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }, + { 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('Should update modifier types correctly when converting to partial', () => { + const A = Type.Object( + { + x: Type.Readonly(Type.Optional(Type.Number())), + y: Type.Readonly(Type.Number()), + z: Type.Optional(Type.Number()), + w: Type.Number(), + }, + { additionalProperties: false }, + ) + const T = Type.Partial(A) + Assert.IsEqual(T.properties.x[ReadonlyKind], 'Readonly') + Assert.IsEqual(T.properties.x[OptionalKind], 'Optional') + Assert.IsEqual(T.properties.y[ReadonlyKind], 'Readonly') + Assert.IsEqual(T.properties.y[OptionalKind], 'Optional') + Assert.IsEqual(T.properties.z[OptionalKind], 'Optional') + Assert.IsEqual(T.properties.w[OptionalKind], 'Optional') + }) + 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.Partial(A) + Assert.IsEqual(A.additionalProperties, false) + Assert.IsEqual(T.additionalProperties, false) + }) +}) diff --git a/test/runtime/compiler-ajv/pick.ts b/test/runtime/compiler-ajv/pick.ts new file mode 100644 index 000000000..c628a1f22 --- /dev/null +++ b/test/runtime/compiler-ajv/pick.ts @@ -0,0 +1,70 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' +import { Assert } from '../assert' + +describe('compiler-ajv/Pick', () => { + it('Should pick properties from the source schema', () => { + const Vector3 = Type.Object( + { + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }, + { 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']) + Assert.IsEqual(T.required!.includes('z'), false) + }) + it('Should delete the required property if no required properties remain', () => { + const A = Type.Object( + { + x: Type.Optional(Type.Number()), + y: Type.Readonly(Type.Optional(Type.Number())), + z: Type.Number(), + }, + { additionalProperties: false }, + ) + const T = Type.Pick(A, ['x', 'y']) + Assert.IsEqual(T.required, undefined) + }) + 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']) + Assert.IsEqual(A.additionalProperties, false) + Assert.IsEqual(T.additionalProperties, false) + }) + + it('Should pick with keyof object', () => { + const A = Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }) + const B = Type.Object({ + x: Type.Number(), + y: Type.Number(), + }) + const T = Type.Pick(A, Type.KeyOf(B), { additionalProperties: false }) + Ok(T, { x: 0, y: 0 }) + Fail(T, { x: 0, y: 0, z: 0 }) + }) +}) diff --git a/test/runtime/compiler-ajv/readonly-optional.ts b/test/runtime/compiler-ajv/readonly-optional.ts new file mode 100644 index 000000000..cbdd883ff --- /dev/null +++ b/test/runtime/compiler-ajv/readonly-optional.ts @@ -0,0 +1,27 @@ +import { Type } from '@sinclair/typebox' +import { Ok } from './validate' +import { Assert } from '../assert' + +describe('compiler-ajv/ReadonlyOptional', () => { + it('Should validate object with optional', () => { + const T = Type.Object( + { + a: Type.Readonly(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.Readonly(Type.Optional(Type.String())), + b: Type.String(), + }, + { additionalProperties: false }, + ) + Assert.IsEqual(T.required!.includes('a'), false) + }) +}) diff --git a/test/runtime/compiler-ajv/readonly.ts b/test/runtime/compiler-ajv/readonly.ts new file mode 100644 index 000000000..c26dd62f6 --- /dev/null +++ b/test/runtime/compiler-ajv/readonly.ts @@ -0,0 +1,27 @@ +import { Type } from '@sinclair/typebox' +import { Ok } from './validate' +import { Assert } from '../assert' + +describe('compiler-ajv/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 }, + ) + Assert.IsEqual(T.required!.includes('a'), true) + Assert.IsEqual(T.required!.includes('b'), true) + }) +}) diff --git a/test/runtime/compiler-ajv/record.ts b/test/runtime/compiler-ajv/record.ts new file mode 100644 index 000000000..0dcba8f8f --- /dev/null +++ b/test/runtime/compiler-ajv/record.ts @@ -0,0 +1,293 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler-ajv/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 not validate when below minProperties', () => { + const T = Type.Record(Type.String(), Type.Number(), { minProperties: 4 }) + Ok(T, { a: 1, b: 2, c: 3, d: 4 }) + Fail(T, { a: 1, b: 2, c: 3 }) + }) + it('Should not validate when above maxProperties', () => { + const T = Type.Record(Type.String(), Type.Number(), { maxProperties: 4 }) + Ok(T, { a: 1, b: 2, c: 3, d: 4 }) + Fail(T, { a: 1, b: 2, c: 3, d: 4, e: 5 }) + }) + it('Should not validate with illogical minProperties | maxProperties', () => { + const T = Type.Record(Type.String(), Type.Number(), { minProperties: 5, maxProperties: 4 }) + Fail(T, { a: 1, b: 2, c: 3 }) + Fail(T, { a: 1, b: 2, c: 3, d: 4 }) + Fail(T, { a: 1, b: 2, c: 3, d: 4, e: 5 }) + }) + it('Should validate when specifying string union literals when additionalProperties is true', () => { + 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 string union literals when additionalProperties is 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 validate for keyof records', () => { + const T = Type.Object({ + a: Type.String(), + b: Type.Number(), + c: Type.String(), + }) + const R = Type.Record(Type.KeyOf(T), Type.Number()) + Ok(R, { a: 1, b: 2, c: 3 }) + }) + it('Should not validate for unknown key via keyof', () => { + const T = Type.Object({ + a: Type.String(), + b: Type.Number(), + c: Type.String(), + }) + const R = Type.Record(Type.KeyOf(T), Type.Number(), { additionalProperties: false }) + Fail(R, { a: 1, b: 2, c: 3, d: 4 }) + }) + it('Should validate when specifying regular expressions', () => { + const K = Type.RegExp(/^op_.*$/) + const T = Type.Record(K, Type.Number()) + Ok(T, { + op_a: 1, + op_b: 2, + op_c: 3, + }) + }) + it('Should not validate when specifying regular expressions and passing invalid property', () => { + const K = Type.RegExp(/^op_.*$/) + const T = Type.Record(K, Type.Number(), { additionalProperties: false }) + Fail(T, { + op_a: 1, + op_b: 2, + aop_c: 3, + }) + }) + it('Should validate with quoted string pattern', () => { + const K = Type.String({ pattern: "'(a|b|c)" }) + const T = Type.Record(K, Type.Number()) + Ok(T, { + "'a": 1, + "'b": 2, + "'c": 3, + }) + }) + it('Should validate with forward-slash pattern', () => { + const K = Type.String({ pattern: '/(a|b|c)' }) + const T = Type.Record(K, Type.Number()) + Ok(T, { + '/a': 1, + '/b': 2, + '/c': 3, + }) + }) + // ------------------------------------------------------------ + // Integer Keys + // ------------------------------------------------------------ + it('Should validate when all property keys are integers', () => { + const T = Type.Record(Type.Integer(), Type.Number()) + Ok(T, { '0': 1, '1': 2, '2': 3, '3': 4 }) + }) + it('Should validate when all property keys are integers, but one property is a string with varying type', () => { + const T = Type.Record(Type.Integer(), Type.Number(), { additionalProperties: false }) + Fail(T, { '0': 1, '1': 2, '2': 3, '3': 4, a: 'hello' }) + }) + it('Should not validate if passing a leading zeros for integers keys', () => { + const T = Type.Record(Type.Integer(), Type.Number(), { additionalProperties: false }) + Fail(T, { + '00': 1, + '01': 2, + '02': 3, + '03': 4, + }) + }) + it('Should not validate if passing a signed integers keys', () => { + const T = Type.Record(Type.Integer(), Type.Number(), { additionalProperties: false }) + Fail(T, { + '-0': 1, + '-1': 2, + '-2': 3, + '-3': 4, + }) + }) + // ------------------------------------------------------------ + // Number Keys + // ------------------------------------------------------------ + 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(), { additionalProperties: false }) + Fail(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', () => { + const T = Type.Record(Type.Number(), Type.Number(), { additionalProperties: false }) + Fail(T, { '0': 1, '1': 2, '2': 3, '3': 4, a: 'hello' }) + }) + // ------------------------------------------------------------ + // AdditionalProperties + // ------------------------------------------------------------ + it('AdditionalProperties 1', () => { + const T = Type.Record(Type.Number(), Type.String(), { additionalProperties: true }) + Ok(T, { 1: '', 2: '', x: 1, y: 2, z: 3 }) + }) + it('AdditionalProperties 2', () => { + const T = Type.Record(Type.Number(), Type.String(), { additionalProperties: false }) + Ok(T, { 1: '', 2: '', 3: '' }) + }) + it('AdditionalProperties 3', () => { + const T = Type.Record(Type.Number(), Type.String(), { additionalProperties: false }) + Fail(T, { 1: '', 2: '', x: '' }) + }) + it('AdditionalProperties 4', () => { + const T = Type.Record(Type.Number(), Type.String(), { additionalProperties: Type.Boolean() }) + Fail(T, { 1: '', 2: '', x: '' }) + }) + it('AdditionalProperties 5', () => { + const T = Type.Record(Type.Number(), Type.String(), { additionalProperties: Type.Boolean() }) + Ok(T, { 1: '', 2: '', x: true }) + }) + // ---------------------------------------------------------------- + // TemplateLiteral + // ---------------------------------------------------------------- + it('TemplateLiteral 1', () => { + const K = Type.TemplateLiteral('key${number}') + const R = Type.Record(K, Type.Number(), { additionalProperties: false }) + Ok(R, { + key0: 1, + key1: 1, + key2: 1, + }) + }) + it('TemplateLiteral 2', () => { + const K = Type.TemplateLiteral('key${number}') + const R = Type.Record(K, Type.Number()) + Ok(R, { keyA: 0 }) + }) + it('TemplateLiteral 3', () => { + const K = Type.TemplateLiteral('key${number}') + const R = Type.Record(K, Type.Number(), { additionalProperties: false }) + Fail(R, { keyA: 0 }) + }) + it('TemplateLiteral 4', () => { + const K = Type.TemplateLiteral('key${number}') + const R = Type.Record(K, Type.Number()) + const T = Type.Object({ x: Type.Number(), y: Type.Number() }) + const I = Type.Intersect([R, T], { unevaluatedProperties: false }) + Ok(I, { + x: 1, + y: 2, + key0: 1, + key1: 1, + key2: 1, + }) + }) + it('TemplateLiteral 5', () => { + const K = Type.TemplateLiteral('key${number}') + const R = Type.Record(K, Type.Number()) + const T = Type.Object({ x: Type.Number(), y: Type.Number() }) + const I = Type.Intersect([R, T]) + Ok(I, { + x: 1, + y: 2, + z: 3, + key0: 1, + key1: 1, + key2: 1, + }) + }) + it('TemplateLiteral 6', () => { + const K = Type.TemplateLiteral('key${number}') + const R = Type.Record(K, Type.Number()) + const T = Type.Object({ x: Type.Number(), y: Type.Number() }) + const I = Type.Intersect([R, T], { unevaluatedProperties: false }) + Fail(I, { + x: 1, + y: 2, + z: 3, + key0: 1, + key1: 1, + key2: 1, + }) + }) + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/916 + // ---------------------------------------------------------------- + it('Should validate for string keys', () => { + const T = Type.Record(Type.String(), Type.Null(), { + additionalProperties: false, + }) + Ok(T, { + a: null, + b: null, + 0: null, + 1: null, + }) + }) + it('Should validate for number keys', () => { + const T = Type.Record(Type.Number(), Type.Null(), { + additionalProperties: false, + }) + Fail(T, { + a: null, + b: null, + 0: null, + 1: null, + }) + Ok(T, { + 0: null, + 1: null, + }) + }) + it('Should validate for any keys', () => { + const T = Type.Record(Type.Any(), Type.Null(), { + additionalProperties: false, + }) + Ok(T, { + a: null, + b: null, + 0: null, + 1: null, + }) + }) + it('Should validate for never keys', () => { + const T = Type.Record(Type.Never(), Type.Null(), { + additionalProperties: false, + }) + Ok(T, {}) + Fail(T, { + a: null, + b: null, + 0: null, + 1: null, + }) + }) +}) diff --git a/test/runtime/compiler-ajv/recursive.ts b/test/runtime/compiler-ajv/recursive.ts new file mode 100644 index 000000000..0a04dc313 --- /dev/null +++ b/test/runtime/compiler-ajv/recursive.ts @@ -0,0 +1,79 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' +import { Assert } from '../assert/index' + +describe('compiler-ajv/Recursive', () => { + it('Should generate default ordinal $id if not specified', () => { + const Node = Type.Recursive((Node) => + Type.Object({ + id: Type.String(), + nodes: Type.Array(Node), + }), + ) + Assert.IsFalse(Node.$id === undefined) + }) + it('Should override default ordinal $id if specified', () => { + const Node = Type.Recursive( + (Node) => + Type.Object({ + id: Type.String(), + nodes: Type.Array(Node), + }), + { $id: 'Node' }, + ) + Assert.IsEqual(Node.$id === 'Node', true) + }) + it('Should validate recursive node type', () => { + const Node = Type.Recursive((This) => + Type.Object({ + id: Type.String(), + nodes: Type.Array(This), + }), + ) + Ok(Node, { + id: 'A', + nodes: [ + { id: 'B', nodes: [] }, + { id: 'C', nodes: [] }, + ], + }) + }) + it('Should validate wrapped recursive node type', () => { + const Node = Type.Tuple([ + Type.Recursive((This) => + Type.Object({ + id: Type.String(), + nodes: Type.Array(This), + }), + ), + ]) + Ok(Node, [ + { + id: 'A', + nodes: [ + { id: 'B', nodes: [] }, + { id: 'C', nodes: [] }, + ], + }, + ]) + }) + it('Should not validate wrapped recursive node type with invalid id', () => { + const Node = Type.Tuple([ + Type.Recursive((This) => + Type.Object({ + id: Type.String(), + nodes: Type.Array(This), + }), + ), + ]) + Fail(Node, [ + { + id: 'A', + nodes: [ + { id: 1, nodes: [] }, + { id: 'C', nodes: [] }, + ], + }, + ]) + }) +}) diff --git a/test/runtime/compiler-ajv/ref.ts b/test/runtime/compiler-ajv/ref.ts new file mode 100644 index 000000000..ac0815b13 --- /dev/null +++ b/test/runtime/compiler-ajv/ref.ts @@ -0,0 +1,78 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler-ajv/Ref', () => { + // ---------------------------------------------------------------- + // Deprecated + // ---------------------------------------------------------------- + it('Should validate for Ref(Schema)', () => { + const T = Type.Number({ $id: 'T' }) + const R = Type.Ref(T) + Ok(R, 1234, [T]) + Fail(R, 'hello', [T]) + }) + // ---------------------------------------------------------------- + // Standard + // ---------------------------------------------------------------- + it('Should should validate when referencing a type', () => { + const T = Type.Object( + { + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }, + { $id: 'T' }, + ) + const R = Type.Ref(T.$id!) + Ok( + R, + { + x: 1, + y: 2, + z: 3, + }, + [T], + ) + }) + it('Should not validate when passing invalid data', () => { + const T = Type.Object( + { + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }, + { $id: 'T' }, + ) + const R = Type.Ref(T.$id!) + Fail( + R, + { + x: 1, + y: 2, + }, + [T], + ) + }) + it('Should de-reference object property schema', () => { + const R = Type.Object( + { + name: Type.String(), + }, + { $id: 'R' }, + ) + + const T = Type.Object( + { + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + r: Type.Optional(Type.Ref(R.$id!)), + }, + { $id: 'T' }, + ) + Ok(T, { x: 1, y: 2, z: 3 }, [R]) + Ok(T, { x: 1, y: 2, z: 3, r: { name: 'hello' } }, [R]) + Fail(T, { x: 1, y: 2, z: 3, r: { name: 1 } }, [R]) + Fail(T, { x: 1, y: 2, z: 3, r: {} }, [R]) + }) +}) diff --git a/test/runtime/compiler-ajv/required.ts b/test/runtime/compiler-ajv/required.ts new file mode 100644 index 000000000..8d1028a27 --- /dev/null +++ b/test/runtime/compiler-ajv/required.ts @@ -0,0 +1,55 @@ +import { Type, ReadonlyKind, OptionalKind } from '@sinclair/typebox' +import { Ok, Fail } from './validate' +import { Assert } from '../assert' + +describe('compiler-ajv/Required', () => { + 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()), + }, + { 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('Should update modifier types correctly when converting to required', () => { + const A = Type.Object({ + x: Type.Readonly(Type.Optional(Type.Number())), + y: Type.Readonly(Type.Number()), + z: Type.Optional(Type.Number()), + w: Type.Number(), + }) + const T = Type.Required(A) + Assert.IsEqual(T.properties.x[ReadonlyKind], 'Readonly') + Assert.IsEqual(T.properties.y[ReadonlyKind], 'Readonly') + Assert.IsEqual(T.properties.z[OptionalKind], undefined) + Assert.IsEqual(T.properties.w[OptionalKind], undefined) + }) + 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()), + }, + { additionalPropeties: false }, + ) + const T = Type.Required(A) + Assert.IsEqual(A.additionalPropeties, false) + Assert.IsEqual(T.additionalPropeties, false) + }) + + // it('Should construct new object when targetting reference', () => { + // const T = Type.Object({ a: Type.String(), b: Type.String() }, { $id: 'T' }) + // const R = Type.Ref(T) + // const P = Type.Required(R) + // strictEqual(P.properties.a.type, 'string') + // strictEqual(P.properties.b.type, 'string') + // }) +}) diff --git a/test/runtime/compiler-ajv/string-pattern.ts b/test/runtime/compiler-ajv/string-pattern.ts new file mode 100644 index 000000000..71419b1dc --- /dev/null +++ b/test/runtime/compiler-ajv/string-pattern.ts @@ -0,0 +1,65 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler-ajv/StringPattern', () => { + //----------------------------------------------------- + // Regular Expression + //----------------------------------------------------- + it('Should validate regular expression 1', () => { + const T = Type.String({ pattern: /[012345]/.source }) + Ok(T, '0') + Ok(T, '1') + Ok(T, '2') + Ok(T, '3') + Ok(T, '4') + Ok(T, '5') + }) + it('Should validate regular expression 2', () => { + const T = Type.String({ pattern: /true|false/.source }) + Ok(T, 'true') + Ok(T, 'true') + Ok(T, 'true') + Ok(T, 'false') + Ok(T, 'false') + Ok(T, 'false') + Fail(T, '6') + }) + it('Should validate regular expression 3', () => { + const T = Type.String({ pattern: /true|false/.source }) + Fail(T, 'unknown') + }) + it('Should validate regular expression 4', () => { + const T = Type.String({ pattern: /[\d]{5}/.source }) + Ok(T, '12345') + }) + //----------------------------------------------------- + // Regular Pattern + //----------------------------------------------------- + it('Should validate pattern 1', () => { + const T = Type.String({ pattern: '[012345]' }) + Ok(T, '0') + Ok(T, '1') + Ok(T, '2') + Ok(T, '3') + Ok(T, '4') + Ok(T, '5') + }) + it('Should validate pattern 2', () => { + const T = Type.String({ pattern: '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 validate pattern 3', () => { + const T = Type.String({ pattern: 'true|false' }) + Fail(T, 'unknown') + }) + it('Should validate pattern 4', () => { + const T = Type.String({ pattern: '[\\d]{5}' }) + Ok(T, '12345') + }) +}) diff --git a/test/runtime/compiler-ajv/string.ts b/test/runtime/compiler-ajv/string.ts new file mode 100644 index 000000000..33729175e --- /dev/null +++ b/test/runtime/compiler-ajv/string.ts @@ -0,0 +1,59 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler-ajv/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('Should validate string format as email', () => { + const T = Type.String({ format: 'email' }) + Ok(T, 'name@domain.com') + }) + it('Should validate string format as uuid', () => { + const T = Type.String({ format: 'uuid' }) + Ok(T, '4a7a17c9-2492-4a53-8e13-06ea2d3f3bbf') + }) + it('Should validate string format as iso8601 date', () => { + const T = Type.String({ format: 'date-time' }) + Ok(T, '2021-06-11T20:30:00-04:00') + }) + it('Should validate minLength', () => { + const T = Type.String({ minLength: 4 }) + Ok(T, '....') + Fail(T, '...') + }) + it('Should validate maxLength', () => { + const T = Type.String({ maxLength: 4 }) + Ok(T, '....') + Fail(T, '.....') + }) + it('Should pass numeric 5 digit test', () => { + const T = Type.String({ pattern: '[\\d]{5}' }) + Ok(T, '12345') + }) +}) diff --git a/test/runtime/compiler-ajv/template-literal.ts b/test/runtime/compiler-ajv/template-literal.ts new file mode 100644 index 000000000..ad083a50b --- /dev/null +++ b/test/runtime/compiler-ajv/template-literal.ts @@ -0,0 +1,185 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler-ajv/TemplateLiteral', () => { + // -------------------------------------------------------- + // Finite + // -------------------------------------------------------- + it('Should validate finite pattern 1', () => { + // prettier-ignore + const T = Type.TemplateLiteral([]) + Ok(T, '') + Fail(T, 'X') + }) + it('Should validate finite pattern 1', () => { + // prettier-ignore + const T = Type.TemplateLiteral([Type.Boolean()]) + Ok(T, 'true') + Ok(T, 'false') + Fail(T, 'X') + }) + it('Should validate finite pattern 2', () => { + // prettier-ignore + const T = Type.TemplateLiteral([ + Type.Literal('A') + ]) + Ok(T, 'A') + Fail(T, 'X') + }) + it('Should validate finite pattern 3', () => { + // prettier-ignore + const T = Type.TemplateLiteral([ + Type.Literal('A'), + Type.Literal('B') + ]) + Ok(T, 'AB') + Fail(T, 'X') + }) + it('Should validate finite pattern 4', () => { + // prettier-ignore + const T = Type.TemplateLiteral([ + Type.Literal('A'), + Type.Union([ + Type.Literal('B'), + Type.Literal('C') + ]), + ]) + Ok(T, 'AB') + Ok(T, 'AC') + Fail(T, 'X') + }) + it('Should validate finite pattern 5', () => { + // prettier-ignore + const T = Type.TemplateLiteral([ + Type.Literal('A'), + Type.Union([ + Type.Literal('B'), + Type.Literal('C') + ]), + Type.Literal('D'), + ]) + Ok(T, 'ABD') + Ok(T, 'ACD') + Fail(T, 'X') + }) + it('Should validate finite pattern 6', () => { + // prettier-ignore + const T = Type.TemplateLiteral([ + Type.Union([ + Type.Literal('0'), + Type.Literal('1') + ]), + Type.Union([ + Type.Literal('0'), + Type.Literal('1') + ]), + ]) + Ok(T, '00') + Ok(T, '01') + Ok(T, '10') + Ok(T, '11') + Fail(T, 'X') + }) + // -------------------------------------------------------- + // Infinite + // -------------------------------------------------------- + it('Should validate infinite pattern 1', () => { + // prettier-ignore + const T = Type.TemplateLiteral([ + Type.Number() + ]) + Ok(T, '1') + Ok(T, '22') + Ok(T, '333') + Ok(T, '4444') + Fail(T, 'X') + }) + it('Should validate infinite pattern 2', () => { + // prettier-ignore + const T = Type.TemplateLiteral([ + Type.Integer() + ]) + Ok(T, '1') + Ok(T, '22') + Ok(T, '333') + Ok(T, '4444') + Fail(T, 'X') + }) + it('Should validate infinite pattern 3', () => { + // prettier-ignore + const T = Type.TemplateLiteral([ + Type.BigInt() + ]) + Ok(T, '1') + Ok(T, '22') + Ok(T, '333') + Ok(T, '4444') + Fail(T, 'X') + }) + it('Should validate infinite pattern 4', () => { + // prettier-ignore + const T = Type.TemplateLiteral([ + Type.String() + ]) + Ok(T, '1') + Ok(T, '22') + Ok(T, '333') + Ok(T, '4444') + Ok(T, 'a') + Ok(T, 'bb') + Ok(T, 'ccc') + Ok(T, 'dddd') + }) + it('Should validate infinite pattern 5', () => { + // prettier-ignore + const T = Type.TemplateLiteral([ + Type.Literal('A'), + Type.Number() + ]) + Ok(T, 'A1') + Ok(T, 'A22') + Ok(T, 'A333') + Ok(T, 'A4444') + Fail(T, 'X') + }) + it('Should validate infinite pattern 6', () => { + // prettier-ignore + const T = Type.TemplateLiteral([ + Type.Literal('A'), + Type.Integer() + ]) + Ok(T, 'A1') + Ok(T, 'A22') + Ok(T, 'A333') + Ok(T, 'A4444') + Fail(T, 'X') + }) + it('Should validate infinite pattern 7', () => { + // prettier-ignore + const T = Type.TemplateLiteral([ + Type.Literal('A'), + Type.BigInt() + ]) + Ok(T, 'A1') + Ok(T, 'A22') + Ok(T, 'A333') + Ok(T, 'A4444') + Fail(T, 'X') + }) + it('Should validate infinite pattern 8', () => { + // prettier-ignore + const T = Type.TemplateLiteral([ + Type.Literal('A'), + Type.String() + ]) + Ok(T, 'A1') + Ok(T, 'A22') + Ok(T, 'A333') + Ok(T, 'A4444') + Ok(T, 'Aa') + Ok(T, 'Abb') + Ok(T, 'Accc') + Ok(T, 'Adddd') + Fail(T, 'X') + }) +}) diff --git a/test/runtime/compiler-ajv/tuple.ts b/test/runtime/compiler-ajv/tuple.ts new file mode 100644 index 000000000..96dfc1aa3 --- /dev/null +++ b/test/runtime/compiler-ajv/tuple.ts @@ -0,0 +1,53 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler-ajv/Tuple', () => { + 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]) + }) + 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('Should validate with empty tuple', () => { + const T = Type.Tuple([]) + Ok(T, []) + }) + it('Should not validate with empty tuple with more items', () => { + const T = Type.Tuple([]) + Fail(T, [1]) + }) + it('Should not validate with empty tuple with less items', () => { + const T = Type.Tuple([Type.Number(), Type.Number()]) + Fail(T, [1]) + }) + 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 }]) + }) + 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/test/runtime/compiler-ajv/uint8array.ts b/test/runtime/compiler-ajv/uint8array.ts new file mode 100644 index 000000000..b50979504 --- /dev/null +++ b/test/runtime/compiler-ajv/uint8array.ts @@ -0,0 +1,49 @@ +// --------------------------------------------------- +// No Longer Supported +// --------------------------------------------------- +// import { Type } from '@sinclair/typebox' +// import { Ok, Fail } from './validate' +// describe('compiler-ajv/Uint8Array', () => { +// it('Should not validate number', () => { +// const T = Type.Uint8Array() +// Fail(T, 1) +// }) +// it('Should not validate string', () => { +// const T = Type.Uint8Array() +// Fail(T, 'hello') +// }) +// it('Should not validate boolean', () => { +// const T = Type.Uint8Array() +// Fail(T, true) +// }) +// it('Should not validate array', () => { +// const T = Type.Uint8Array() +// Fail(T, [1, 2, 3]) +// }) +// it('Should not validate object', () => { +// const T = Type.Uint8Array() +// Fail(T, { a: 1, b: 2 }) +// }) +// it('Should not validate null', () => { +// const T = Type.Uint8Array() +// Fail(T, null) +// }) +// it('Should not validate undefined', () => { +// const T = Type.Uint8Array() +// Fail(T, undefined) +// }) +// it('Should validate Uint8Array', () => { +// const T = Type.Uint8Array() +// Ok(T, new Uint8Array(100)) +// }) +// it('Should validate minByteLength', () => { +// const T = Type.Uint8Array({ minByteLength: 4 }) +// Ok(T, new Uint8Array(4)) +// Fail(T, new Uint8Array(3)) +// }) +// it('Should validate maxByteLength', () => { +// const T = Type.Uint8Array({ maxByteLength: 4 }) +// Ok(T, new Uint8Array(4)) +// Fail(T, new Uint8Array(5)) +// }) +// }) diff --git a/test/runtime/compiler-ajv/union.ts b/test/runtime/compiler-ajv/union.ts new file mode 100644 index 000000000..ab18c1e18 --- /dev/null +++ b/test/runtime/compiler-ajv/union.ts @@ -0,0 +1,56 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler-ajv/Union', () => { + 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) + }) + 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('Should fail to validate for descriminated union types', () => { + const A = Type.Object({ kind: Type.Literal('A'), value: Type.String() }) + const B = Type.Object({ kind: Type.Literal('B'), value: Type.Number() }) + const T = Type.Union([A, B]) + Fail(T, { kind: 'A', value: 42 }) // expect { kind: 'A', value: string } + Fail(T, { kind: 'B', value: 'hello' }) // expect { kind: 'B', value: number } + }) + 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 + }) + 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('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 + }) + 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/test/runtime/compiler-ajv/unknown.ts b/test/runtime/compiler-ajv/unknown.ts new file mode 100644 index 000000000..8ad7cfacf --- /dev/null +++ b/test/runtime/compiler-ajv/unknown.ts @@ -0,0 +1,33 @@ +import { Type } from '@sinclair/typebox' +import { Ok } from './validate' + +describe('compiler-ajv/Unknown', () => { + 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/test/runtime/compiler-ajv/unsafe.ts b/test/runtime/compiler-ajv/unsafe.ts new file mode 100644 index 000000000..6a53eea0a --- /dev/null +++ b/test/runtime/compiler-ajv/unsafe.ts @@ -0,0 +1,18 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler-ajv/Unsafe', () => { + it('Should validate an unsafe type', () => { + const T = Type.Unsafe({ + type: 'object', + properties: { + x: { type: 'number' }, + y: { type: 'number' }, + z: { type: 'number' }, + }, + additionalProperties: false, + }) + Ok(T, { x: 1, y: 2, z: 3 }) + Fail(T, { x: 1, y: 2, z: 3, w: 4 }) + }) +}) diff --git a/test/runtime/compiler-ajv/validate.ts b/test/runtime/compiler-ajv/validate.ts new file mode 100644 index 000000000..baf279db8 --- /dev/null +++ b/test/runtime/compiler-ajv/validate.ts @@ -0,0 +1,91 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Value } from '@sinclair/typebox/value' + +import { TSchema } from '@sinclair/typebox' +import addFormats from 'ajv-formats' +import Ajv, { AnySchema } from 'ajv/dist/2019' + +function schemaOf(schemaOf: string, value: unknown, schema: unknown) { + switch (schemaOf) { + case 'Constructor': + return TypeGuard.IsConstructor(schema) && Value.Check(schema, value) + case 'Function': + return TypeGuard.IsFunction(schema) && Value.Check(schema, value) + case 'Date': + return TypeGuard.IsDate(schema) && Value.Check(schema, value) + case 'Promise': + return TypeGuard.IsPromise(schema) && Value.Check(schema, value) + case 'Uint8Array': + return TypeGuard.IsUint8Array(schema) && Value.Check(schema, value) + case 'Undefined': + return TypeGuard.IsUndefined(schema) && Value.Check(schema, value) + case 'Void': + return TypeGuard.IsVoid(schema) && Value.Check(schema, value) + default: + return false + } +} +export function createAjv(references: AnySchema[]) { + 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({ type: 'object', keyword: 'instanceOf', validate: schemaOf }) + .addKeyword({ type: 'null', keyword: 'typeOf', validate: schemaOf }) + .addKeyword('exclusiveMinimumTimestamp') + .addKeyword('exclusiveMaximumTimestamp') + .addKeyword('minimumTimestamp') + .addKeyword('maximumTimestamp') + .addKeyword('minByteLength') + .addKeyword('maxByteLength') + .addSchema(references) +} +export function Ok(type: T, data: unknown, additional: AnySchema[] = []) { + const ajv = createAjv(additional) + function execute() { + // required as ajv will throw if referenced schema is not found + try { + return ajv.validate(type, data) as boolean + } catch { + return false + } + } + if (execute() === 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, additional: AnySchema[] = []) { + const ajv = createAjv(additional) + function execute() { + // required as ajv will throw if referenced schema is not found + try { + return ajv.validate(type, data) as boolean + } catch { + return false + } + } + if (execute() === 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('---------------------------') + console.log('errors') + console.log('---------------------------') + console.log(ajv.errorsText(ajv.errors)) + throw Error('expected ok') + } +} diff --git a/test/runtime/compiler-ajv/void.ts b/test/runtime/compiler-ajv/void.ts new file mode 100644 index 000000000..547ead1ae --- /dev/null +++ b/test/runtime/compiler-ajv/void.ts @@ -0,0 +1,33 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler-ajv/Void', () => { + it('Should not validate number', () => { + const T = Type.Void() + Fail(T, 1) + }) + it('Should not validate string', () => { + const T = Type.Void() + Fail(T, 'hello') + }) + it('Should not validate boolean', () => { + const T = Type.Void() + Fail(T, true) + }) + it('Should not validate array', () => { + const T = Type.Void() + Fail(T, [1, 2, 3]) + }) + it('Should not validate object', () => { + const T = Type.Void() + Fail(T, { a: 1, b: 2 }) + }) + it('Should validate null', () => { + const T = Type.Null() + Ok(T, null) + }) + it('Should not validate undefined', () => { + const T = Type.Void() + Fail(T, undefined) + }) +}) diff --git a/test/runtime/compiler/__members.ts b/test/runtime/compiler/__members.ts new file mode 100644 index 000000000..6ade7f40b --- /dev/null +++ b/test/runtime/compiler/__members.ts @@ -0,0 +1,20 @@ +import { TypeCompiler } from '@sinclair/typebox/compiler' +import { Type, TypeGuard, ValueGuard } from '@sinclair/typebox' +import { Assert } from '../assert/index' + +describe('compiler/TypeCheckMembers', () => { + it('Should return Schema', () => { + const A = TypeCompiler.Compile(Type.Number(), [Type.String(), Type.Boolean()]) + Assert.IsTrue(TypeGuard.IsNumber(A.Schema())) + }) + it('Should return References', () => { + const A = TypeCompiler.Compile(Type.Number(), [Type.String(), Type.Boolean()]) + Assert.IsTrue(TypeGuard.IsNumber(A.Schema())) + Assert.IsTrue(TypeGuard.IsString(A.References()[0])) + Assert.IsTrue(TypeGuard.IsBoolean(A.References()[1])) + }) + it('Should return Code', () => { + const A = TypeCompiler.Compile(Type.Number(), [Type.String(), Type.Boolean()]) + Assert.IsTrue(ValueGuard.IsString(A.Code())) + }) +}) diff --git a/test/runtime/compiler/any.ts b/test/runtime/compiler/any.ts new file mode 100644 index 000000000..fb4c7de56 --- /dev/null +++ b/test/runtime/compiler/any.ts @@ -0,0 +1,41 @@ +import { Type } from '@sinclair/typebox' +import { Ok } from './validate' + +describe('compiler/Any', () => { + 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) + }) + it('Should validate bigint', () => { + const T = Type.Any() + Ok(T, BigInt(1)) + }) + it('Should validate symbol', () => { + const T = Type.Any() + Ok(T, Symbol(1)) + }) +}) diff --git a/test/runtime/compiler/argument.ts b/test/runtime/compiler/argument.ts new file mode 100644 index 000000000..502df49c6 --- /dev/null +++ b/test/runtime/compiler/argument.ts @@ -0,0 +1,41 @@ +import { Type } from '@sinclair/typebox' +import { Ok } from './validate' + +describe('compiler/Argument', () => { + it('Should validate number', () => { + const T = Type.Argument(0) + Ok(T, 1) + }) + it('Should validate string', () => { + const T = Type.Argument(0) + Ok(T, 'hello') + }) + it('Should validate boolean', () => { + const T = Type.Argument(0) + Ok(T, true) + }) + it('Should validate array', () => { + const T = Type.Argument(0) + Ok(T, [1, 2, 3]) + }) + it('Should validate object', () => { + const T = Type.Argument(0) + Ok(T, { a: 1, b: 2 }) + }) + it('Should validate null', () => { + const T = Type.Argument(0) + Ok(T, null) + }) + it('Should validate undefined', () => { + const T = Type.Argument(0) + Ok(T, undefined) + }) + it('Should validate bigint', () => { + const T = Type.Argument(0) + Ok(T, BigInt(1)) + }) + it('Should validate symbol', () => { + const T = Type.Argument(0) + Ok(T, Symbol(1)) + }) +}) diff --git a/test/runtime/compiler/array.ts b/test/runtime/compiler/array.ts new file mode 100644 index 000000000..70af4c500 --- /dev/null +++ b/test/runtime/compiler/array.ts @@ -0,0 +1,186 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler/Array', () => { + it('Should validate an array of any', () => { + const T = Type.Array(Type.Any()) + Ok(T, [0, true, 'hello', {}]) + }) + it('Should not validate varying array when item is number', () => { + const T = Type.Array(Type.Number()) + Fail(T, [1, 2, 3, 'hello']) + }) + 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('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('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]) + 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 composite types when passing additionalProperties false', () => { + const A = Type.Object({ a: Type.String() }) + const B = Type.Object({ b: Type.String() }) + const C = Type.Composite([A, B], { additionalProperties: 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'], + ]) + }) + it('Should not validate array with failed minItems', () => { + const T = Type.Array(Type.Number(), { minItems: 3 }) + Fail(T, [0, 1]) + }) + it('Should not validate array with failed maxItems', () => { + const T = Type.Array(Type.Number(), { maxItems: 3 }) + Fail(T, [0, 1, 2, 3]) + }) + // --------------------------------------------------------- + // Unique Items + // --------------------------------------------------------- + it('Should validate array with uniqueItems when items are distinct objects', () => { + const T = Type.Array(Type.Object({ x: Type.Number(), y: Type.Number() }), { uniqueItems: true }) + Ok(T, [ + { x: 0, y: 1 }, + { x: 1, y: 0 }, + ]) + }) + it('Should not validate array with uniqueItems when items are not distinct objects', () => { + const T = Type.Array(Type.Object({ x: Type.Number(), y: Type.Number() }), { uniqueItems: true }) + Fail(T, [ + { x: 1, y: 0 }, + { x: 1, y: 0 }, + ]) + }) + it('Should not validate array with non uniqueItems', () => { + const T = Type.Array(Type.Number(), { uniqueItems: true }) + Fail(T, [0, 0]) + }) + // --------------------------------------------------------- + // Contains + // --------------------------------------------------------- + it('Should validate for contains', () => { + const T = Type.Array(Type.Number(), { contains: Type.Literal(1) }) + Ok(T, [1]) + Ok(T, [1, 2]) + Fail(T, []) + Fail(T, [2]) + }) + it('Should validate for minContains', () => { + const T = Type.Array(Type.Number(), { contains: Type.Literal(1), minContains: 3 }) + Ok(T, [1, 1, 1, 2]) + Ok(T, [2, 1, 1, 1, 2]) + Ok(T, [1, 1, 1]) + Fail(T, []) + Fail(T, [1, 1]) + Fail(T, [2]) + }) + it('Should validate for maxContains', () => { + const T = Type.Array(Type.Number(), { contains: Type.Literal(1), maxContains: 3 }) + Ok(T, [1, 1, 1]) + Ok(T, [1, 1]) + Ok(T, [2, 2, 2, 2, 1, 1, 1]) + Fail(T, [1, 1, 1, 1]) + }) + it('Should validate for minContains and maxContains', () => { + const T = Type.Array(Type.Number(), { contains: Type.Literal(1), minContains: 3, maxContains: 5 }) + Fail(T, [1, 1]) + Ok(T, [1, 1, 1]) + Ok(T, [1, 1, 1, 1]) + Ok(T, [1, 1, 1, 1, 1]) + Fail(T, [1, 1, 1, 1, 1, 1]) + }) + it('Should not validate minContains or maxContains when contains is unspecified', () => { + const T = Type.Array(Type.Number(), { minContains: 3, maxContains: 5 }) + Fail(T, [1, 1]) + Fail(T, [1, 1, 1]) + Fail(T, [1, 1, 1, 1]) + Fail(T, [1, 1, 1, 1, 1]) + Fail(T, [1, 1, 1, 1, 1, 1]) + }) + it('Should produce illogical schema when contains is not sub type of items', () => { + const T = Type.Array(Type.Number(), { contains: Type.String(), minContains: 3, maxContains: 5 }) + Fail(T, [1, 1]) + Fail(T, [1, 1, 1]) + Fail(T, [1, 1, 1, 1]) + Fail(T, [1, 1, 1, 1, 1]) + Fail(T, [1, 1, 1, 1, 1, 1]) + }) + // ---------------------------------------------------------------- + // Issue: https://github.com/sinclairzx81/typebox/discussions/607 + // ---------------------------------------------------------------- + it('Should correctly handle undefined array properties', () => { + const Answer = Type.Object({ + text: Type.String(), + isCorrect: Type.Boolean(), + }) + const Question = Type.Object({ + text: Type.String(), + options: Type.Array(Answer, { + minContains: 1, + maxContains: 1, + contains: Type.Object({ + text: Type.String(), + isCorrect: Type.Literal(true), + }), + }), + }) + Fail(Question, { text: 'A' }) + Fail(Question, { text: 'A', options: [] }) + Ok(Question, { text: 'A', options: [{ text: 'A', isCorrect: true }] }) + Ok(Question, { + text: 'A', + options: [ + { text: 'A', isCorrect: true }, + { text: 'B', isCorrect: false }, + ], + }) + Fail(Question, { text: 'A', options: [{ text: 'A', isCorrect: false }] }) + Fail(Question, { + text: 'A', + options: [ + { text: 'A', isCorrect: true }, + { text: 'B', isCorrect: true }, + ], + }) + }) +}) diff --git a/test/runtime/compiler/async-iterator.ts b/test/runtime/compiler/async-iterator.ts new file mode 100644 index 000000000..52dcf5e8d --- /dev/null +++ b/test/runtime/compiler/async-iterator.ts @@ -0,0 +1,20 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler/AsyncIterator', () => { + it('Should validate a async iterator 1', () => { + async function* f() {} + const T = Type.AsyncIterator(Type.Any()) + Ok(T, f()) + }) + it('Should validate a async iterator 2', () => { + const T = Type.AsyncIterator(Type.Any()) + Ok(T, { + [Symbol.asyncIterator]: () => {}, + }) + }) + it('Should not validate a async iterator', () => { + const T = Type.AsyncIterator(Type.Any()) + Fail(T, {}) + }) +}) diff --git a/test/runtime/compiler/bigint.ts b/test/runtime/compiler/bigint.ts new file mode 100644 index 000000000..2e9c48a4e --- /dev/null +++ b/test/runtime/compiler/bigint.ts @@ -0,0 +1,80 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler/BigInt', () => { + it('Should not validate number', () => { + const T = Type.BigInt() + Fail(T, 3.14) + }) + it('Should not validate NaN', () => { + const T = Type.BigInt() + Fail(T, NaN) + }) + it('Should not validate +Infinity', () => { + const T = Type.BigInt() + Fail(T, Infinity) + }) + it('Should not validate -Infinity', () => { + const T = Type.BigInt() + Fail(T, -Infinity) + }) + it('Should not validate integer', () => { + const T = Type.BigInt() + Fail(T, 1) + }) + it('Should not validate string', () => { + const T = Type.BigInt() + Fail(T, 'hello') + }) + it('Should not validate boolean', () => { + const T = Type.BigInt() + Fail(T, true) + }) + it('Should not validate array', () => { + const T = Type.BigInt() + Fail(T, [1, 2, 3]) + }) + it('Should not validate object', () => { + const T = Type.BigInt() + Fail(T, { a: 1, b: 2 }) + }) + it('Should not validate null', () => { + const T = Type.BigInt() + Fail(T, null) + }) + it('Should not validate undefined', () => { + const T = Type.BigInt() + Fail(T, undefined) + }) + it('Should not validate symbol', () => { + const T = Type.BigInt() + Fail(T, Symbol()) + }) + it('Should validate bigint', () => { + const T = Type.BigInt() + Ok(T, BigInt(1)) + }) + it('Should validate minimum', () => { + const T = Type.BigInt({ minimum: BigInt(10) }) + Fail(T, BigInt(9)) + Ok(T, BigInt(10)) + }) + it('Should validate maximum', () => { + const T = Type.BigInt({ maximum: BigInt(10) }) + Ok(T, BigInt(10)) + Fail(T, BigInt(11)) + }) + it('Should validate Date exclusiveMinimum', () => { + const T = Type.BigInt({ exclusiveMinimum: BigInt(10) }) + Fail(T, BigInt(10)) + Ok(T, BigInt(11)) + }) + it('Should validate Date exclusiveMaximum', () => { + const T = Type.BigInt({ exclusiveMaximum: BigInt(10) }) + Ok(T, BigInt(9)) + Fail(T, BigInt(10)) + }) + it('Should not validate NaN', () => { + Fail(Type.Number(), NaN) + }) +}) diff --git a/test/runtime/compiler/boolean.ts b/test/runtime/compiler/boolean.ts new file mode 100644 index 000000000..e401585b1 --- /dev/null +++ b/test/runtime/compiler/boolean.ts @@ -0,0 +1,42 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler/Boolean', () => { + 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) + }) + it('Should not validate bigint', () => { + const T = Type.Boolean() + Fail(T, BigInt(1)) + }) + it('Should not validate symbol', () => { + const T = Type.Boolean() + Fail(T, Symbol(1)) + }) +}) diff --git a/test/runtime/compiler/composite.ts b/test/runtime/compiler/composite.ts new file mode 100644 index 000000000..960a1d7f1 --- /dev/null +++ b/test/runtime/compiler/composite.ts @@ -0,0 +1,101 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler/Composite', () => { + it('Should compose two objects', () => { + const A = Type.Object({ a: Type.String() }) + const B = Type.Object({ b: Type.Number() }) + const T = Type.Composite([A, B], { additionalProperties: false }) + Ok(T, { a: 'hello', b: 42 }) + }) + it('Should compose with partial', () => { + const A = Type.Partial(Type.Object({ a: Type.Number() })) + const B = Type.Partial(Type.Object({ b: Type.Number() })) + const P = Type.Composite([A, B], { additionalProperties: false }) + Ok(P, { a: 1, b: 2 }) + Ok(P, { a: 1 }) + Ok(P, { b: 1 }) + Ok(P, {}) + Fail(P, { a: 1, b: 2, c: 3 }) + Fail(P, { c: 1 }) + }) + it('Should compose with overlapping same type', () => { + const A = Type.Object({ a: Type.Number() }) + const B = Type.Object({ a: Type.Number() }) + const P = Type.Composite([A, B]) + Ok(P, { a: 1 }) + Fail(P, { a: '1' }) + }) + it('Should not compose with overlapping varying type', () => { + const A = Type.Object({ a: Type.Number() }) + const B = Type.Object({ a: Type.String() }) + const T = Type.Composite([A, B]) + Fail(T, { a: 1 }) + Fail(T, { a: 'hello' }) + Fail(T, {}) + }) + it('Should compose with deeply nest overlapping varying type', () => { + const A = Type.Object({ a: Type.Number() }) + const B = Type.Object({ b: Type.String() }) + const C = Type.Object({ c: Type.Boolean() }) + const D = Type.Object({ d: Type.Null() }) + const T = Type.Composite([A, B, C, D]) + Ok(T, { a: 1, b: 'hello', c: true, d: null }) + }) + it('Should not compose with deeply nest overlapping varying type', () => { + const A = Type.Object({ a: Type.Number() }) + const B = Type.Object({ a: Type.String() }) + const C = Type.Object({ a: Type.Boolean() }) + const D = Type.Object({ a: Type.Null() }) + const T = Type.Composite([A, B, C, D]) + Fail(T, { a: 1 }) + Fail(T, { a: 'hello' }) + Fail(T, { a: false }) + Fail(T, { a: null }) + Fail(T, { a: [] }) + Fail(T, {}) + }) + it('Should pick from composited type', () => { + const A = Type.Object({ x: Type.Number() }) + const B = Type.Object({ y: Type.Number() }) + const C = Type.Object({ z: Type.Number() }) + const T = Type.Composite([A, B, C]) + const P = Type.Pick(T, ['x', 'y'], { additionalProperties: false }) + Ok(P, { x: 1, y: 1 }) + Fail(P, { x: 1, y: 1, z: 1 }) + }) + it('Should omit from composited type', () => { + const A = Type.Object({ x: Type.Number() }) + const B = Type.Object({ y: Type.Number() }) + const C = Type.Object({ z: Type.Number() }) + const T = Type.Composite([A, B, C]) + const P = Type.Omit(T, ['z'], { additionalProperties: false }) + Ok(P, { x: 1, y: 1 }) + Fail(P, { x: 1, y: 1, z: 1 }) + }) + it('Should compose nested object properties', () => { + const A = Type.Object({ x: Type.Object({ x: Type.Number() }) }) + const B = Type.Object({ y: Type.Object({ x: Type.String() }) }) + const T = Type.Composite([A, B]) + Ok(T, { x: { x: 1 }, y: { x: '' } }) + Fail(T, { x: { x: '1' }, y: { x: '' } }) + Fail(T, { x: { x: 1 }, y: { x: 1 } }) + }) + // prettier-ignore + it('Should composite intersection', () => { + const T = Type.Composite([ + Type.Intersect([ + Type.Object({ x: Type.Number() }) + ]), + Type.Intersect([ + Type.Object({ y: Type.Number() }) + ]), + Type.Intersect([ + Type.Object({ z: Type.Number() }) + ]), + ]) + Ok(T, { x: 1, y: 2, z: 3 }) + Fail(T, { x: 1, y: 2, z: '3' }) + Fail(T, { x: 1, y: 2 }) + }) +}) diff --git a/test/runtime/compiler/const.ts b/test/runtime/compiler/const.ts new file mode 100644 index 000000000..a8526757e --- /dev/null +++ b/test/runtime/compiler/const.ts @@ -0,0 +1,39 @@ +import { Type } from '@sinclair/typebox' +import { Ok } from './validate' + +describe('compiler/Const', () => { + it('Should validate 1', () => { + const T = Type.Const(1) + Ok(T, 1) + }) + it('Should validate 2', () => { + const T = Type.Const('hello') + Ok(T, 'hello') + }) + it('Should validate 3', () => { + const T = Type.Const(true) + Ok(T, true) + }) + it('Should validate 4', () => { + const T = Type.Const({ x: 1, y: 2 }) + Ok(T, { x: 1, y: 2 }) + }) + it('Should validate 5', () => { + const T = Type.Const([1, 2, 3]) + Ok(T, [1, 2, 3]) + }) + it('Should validate 6', () => { + const T = Type.Const([1, true, 'hello']) + Ok(T, [1, true, 'hello']) + }) + it('Should validate 7', () => { + const T = Type.Const({ + x: [1, 2, 3, 4], + y: { x: 1, y: 2, z: 3 }, + }) + Ok(T, { + x: [1, 2, 3, 4], + y: { x: 1, y: 2, z: 3 }, + }) + }) +}) diff --git a/test/runtime/compiler/constructor.ts b/test/runtime/compiler/constructor.ts new file mode 100644 index 000000000..324faf111 --- /dev/null +++ b/test/runtime/compiler/constructor.ts @@ -0,0 +1,80 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler/Constructor', () => { + it('Should validate constructor 1', () => { + const T = Type.Constructor([], Type.Object({})) + Ok(T, class {}) + }) + it('Should validate constructor 2', () => { + const T = Type.Constructor([Type.Number()], Type.Object({})) + // note: constructor arguments are non-checkable + Ok(T, class {}) + }) + it('Should validate constructor 3', () => { + const T = Type.Constructor( + [Type.Number()], + Type.Object({ + method: Type.Function([], Type.Void()), + }), + ) + Ok( + T, + class { + method() {} + }, + ) + }) + it('Should validate constructor 4', () => { + const T = Type.Constructor( + [Type.Number()], + Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }), + ) + Ok( + T, + class { + get x() { + return 1 + } + get y() { + return 1 + } + get z() { + return 1 + } + }, + ) + }) + it('Should not validate constructor 1', () => { + const T = Type.Constructor([Type.Number()], Type.Object({})) + Fail(T, 1) + }) + it('Should not validate constructor 2', () => { + const T = Type.Constructor( + [Type.Number()], + Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }), + ) + Fail( + T, + class { + get x() { + return null + } + get y() { + return null + } + get z() { + return null + } + }, + ) + }) +}) diff --git a/test/runtime/compiler/date.ts b/test/runtime/compiler/date.ts new file mode 100644 index 000000000..b4425f63c --- /dev/null +++ b/test/runtime/compiler/date.ts @@ -0,0 +1,74 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler/Date', () => { + it('Should not validate number', () => { + const T = Type.Date() + Fail(T, 1) + }) + it('Should not validate string', () => { + const T = Type.Date() + Fail(T, 'hello') + }) + it('Should not validate boolean', () => { + const T = Type.Date() + Fail(T, true) + }) + it('Should not validate array', () => { + const T = Type.Date() + Fail(T, [1, 2, 3]) + }) + it('Should not validate object', () => { + const T = Type.Date() + Fail(T, { a: 1, b: 2 }) + }) + it('Should not validate null', () => { + const T = Type.Date() + Fail(T, null) + }) + it('Should not validate undefined', () => { + const T = Type.Date() + Fail(T, undefined) + }) + it('Should validate Date', () => { + const T = Type.Date() + Ok(T, new Date()) + }) + it('Should not validate bigint', () => { + const T = Type.Date() + Fail(T, BigInt(1)) + }) + it('Should not validate symbol', () => { + const T = Type.Date() + Fail(T, Symbol(1)) + }) + it('Should not validate Date if is invalid', () => { + const T = Type.Date() + Fail(T, new Date('not-a-valid-date')) + }) + it('Should validate Date minimumTimestamp', () => { + const T = Type.Date({ minimumTimestamp: 10 }) + Fail(T, new Date(9)) + Ok(T, new Date(10)) + }) + it('Should validate Date maximumTimestamp', () => { + const T = Type.Date({ maximumTimestamp: 10 }) + Fail(T, new Date(11)) + Ok(T, new Date(10)) + }) + it('Should validate Date exclusiveMinimumTimestamp', () => { + const T = Type.Date({ exclusiveMinimumTimestamp: 10 }) + Fail(T, new Date(10)) + Ok(T, new Date(11)) + }) + it('Should validate Date exclusiveMaximumTimestamp', () => { + const T = Type.Date({ exclusiveMaximumTimestamp: 10 }) + Fail(T, new Date(10)) + Ok(T, new Date(9)) + }) + it('Should validate Date multipleOfTimestamp', () => { + const T = Type.Date({ multipleOfTimestamp: 2 }) + Fail(T, new Date(1)) + Ok(T, new Date(2)) + }) +}) diff --git a/test/runtime/compiler/enum.ts b/test/runtime/compiler/enum.ts new file mode 100644 index 000000000..679e4ac9c --- /dev/null +++ b/test/runtime/compiler/enum.ts @@ -0,0 +1,53 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler/Enum', () => { + 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) + }) + 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') + }) + 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') + }) + 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) + }) + 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) + }) +}) diff --git a/test/runtime/compiler/function.ts b/test/runtime/compiler/function.ts new file mode 100644 index 000000000..29a604b96 --- /dev/null +++ b/test/runtime/compiler/function.ts @@ -0,0 +1,25 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler/Function', () => { + it('Should validate function 1', () => { + const T = Type.Function([Type.Number()], Type.Number()) + Ok(T, function () {}) + }) + it('Should validate function 2', () => { + const T = Type.Function([Type.Number()], Type.Number()) + // note: validation only checks typeof 'function' + Ok(T, function (a: string, b: string, c: string, d: string) {}) + }) + it('Should validate function 3', () => { + const T = Type.Function([Type.Number()], Type.Number()) + // note: validation only checks typeof 'function' + Ok(T, function () { + return 'not-a-number' + }) + }) + it('Should not validate function', () => { + const T = Type.Function([Type.Number()], Type.Number()) + Fail(T, 1) + }) +}) diff --git a/test/runtime/compiler/index.ts b/test/runtime/compiler/index.ts new file mode 100644 index 000000000..4bb587c30 --- /dev/null +++ b/test/runtime/compiler/index.ts @@ -0,0 +1,46 @@ +import './__members' +import './any' +import './argument' +import './array' +import './async-iterator' +import './bigint' +import './boolean' +import './composite' +import './const' +import './constructor' +import './date' +import './unicode' +import './enum' +import './function' +import './integer' +import './intersect' +import './iterator' +import './keyof' +import './kind' +import './literal' +import './module' +import './never' +import './not' +import './null' +import './number' +import './object' +import './omit' +import './optional' +import './partial' +import './pick' +import './readonly-optional' +import './readonly' +import './recursive' +import './record' +import './ref' +import './regexp' +import './required' +import './string-pattern' +import './string' +import './symbol' +import './template-literal' +import './tuple' +import './uint8array' +import './union' +import './unknown' +import './void' diff --git a/test/runtime/compiler/integer.ts b/test/runtime/compiler/integer.ts new file mode 100644 index 000000000..51720440e --- /dev/null +++ b/test/runtime/compiler/integer.ts @@ -0,0 +1,77 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler/Integer', () => { + it('Should not validate number', () => { + const T = Type.Integer() + Fail(T, 3.14) + }) + it('Should not validate NaN', () => { + const T = Type.Integer() + Fail(T, NaN) + }) + it('Should not validate +Infinity', () => { + const T = Type.Integer() + Fail(T, Infinity) + }) + it('Should not validate -Infinity', () => { + const T = Type.Integer() + Fail(T, -Infinity) + }) + it('Should validate integer', () => { + const T = Type.Integer() + Ok(T, 1) + }) + it('Should not validate string', () => { + const T = Type.Integer() + Fail(T, 'hello') + }) + it('Should not validate boolean', () => { + const T = Type.Integer() + Fail(T, true) + }) + it('Should not validate array', () => { + const T = Type.Integer() + Fail(T, [1, 2, 3]) + }) + it('Should not validate object', () => { + const T = Type.Integer() + Fail(T, { a: 1, b: 2 }) + }) + it('Should not validate null', () => { + const T = Type.Integer() + Fail(T, null) + }) + it('Should not validate undefined', () => { + const T = Type.Integer() + Fail(T, undefined) + }) + it('Should not validate bigint', () => { + const T = Type.Integer() + Fail(T, BigInt(1)) + }) + it('Should not validate symbol', () => { + const T = Type.Integer() + Fail(T, Symbol(1)) + }) + it('Should validate minimum', () => { + const T = Type.Integer({ minimum: 10 }) + Fail(T, 9) + Ok(T, 10) + }) + it('Should validate maximum', () => { + const T = Type.Integer({ maximum: 10 }) + Ok(T, 10) + Fail(T, 11) + }) + it('Should validate Date exclusiveMinimum', () => { + const T = Type.Integer({ exclusiveMinimum: 10 }) + Fail(T, 10) + Ok(T, 11) + }) + it('Should validate Date exclusiveMaximum', () => { + const T = Type.Integer({ exclusiveMaximum: 10 }) + Ok(T, 9) + Fail(T, 10) + }) +}) diff --git a/test/runtime/compiler/intersect.ts b/test/runtime/compiler/intersect.ts new file mode 100644 index 000000000..652e3be7f --- /dev/null +++ b/test/runtime/compiler/intersect.ts @@ -0,0 +1,225 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler/Intersect', () => { + it('Should intersect number and number', () => { + const A = Type.Number() + const B = Type.Number() + const T = Type.Intersect([A, B], {}) + Ok(T, 1) + }) + it('Should not intersect string and number', () => { + const A = Type.String() + const B = Type.Number() + const T = Type.Intersect([A, B], {}) + Fail(T, 1) + Fail(T, '1') + }) + it('Should intersect two objects', () => { + const A = Type.Object({ x: Type.Number() }) + const B = Type.Object({ y: Type.Number() }) + const T = Type.Intersect([A, B], {}) + Ok(T, { x: 1, y: 1 }) + }) + it('Should not intersect two objects with internal additionalProperties false', () => { + const A = Type.Object({ x: Type.Number() }, { additionalProperties: false }) + const B = Type.Object({ y: Type.Number() }, { additionalProperties: false }) + const T = Type.Intersect([A, B], {}) + Fail(T, { x: 1, y: 1 }) + }) + it('Should intersect two objects and mandate required properties', () => { + const A = Type.Object({ x: Type.Number() }) + const B = Type.Object({ y: Type.Number() }) + const T = Type.Intersect([A, B], {}) + Ok(T, { x: 1, y: 1 }) + Fail(T, { x: 1 }) + Fail(T, { y: 1 }) + }) + it('Should intersect two objects with unevaluated properties', () => { + const A = Type.Object({ x: Type.Number() }) + const B = Type.Object({ y: Type.Number() }) + const T = Type.Intersect([A, B], {}) + Ok(T, { x: 1, y: 2, z: 1 }) + }) + it('Should intersect two objects and restrict unevaluated properties', () => { + const A = Type.Object({ x: Type.Number() }) + const B = Type.Object({ y: Type.Number() }) + const T = Type.Intersect([A, B], { unevaluatedProperties: false }) + Fail(T, { x: 1, y: 2, z: 1 }) + }) + it('Should intersect two objects and allow unevaluated properties of number', () => { + const A = Type.Object({ x: Type.Number() }) + const B = Type.Object({ y: Type.Number() }) + const T = Type.Intersect([A, B], { unevaluatedProperties: Type.Number() }) + Ok(T, { x: 1, y: 2, z: 3 }) + Fail(T, { x: 1, y: 2, z: '1' }) + }) + it('Should intersect two nested objects and allow unevaluated properties of number', () => { + const A = Type.Object({ x: Type.Number() }) + const B = Type.Object({ y: Type.Number() }) + const T = Type.Object({ nested: Type.Intersect([A, B], { unevaluatedProperties: Type.Number() }) }) + Ok(T, { nested: { x: 1, y: 2, z: 3 } }) + Fail(T, { nested: { x: 1, y: 2, z: '1' } }) + }) + it('Should intersect two union objects with overlapping properties of the same type', () => { + const A = Type.Union([Type.Object({ x: Type.Number() })]) + const B = Type.Union([Type.Object({ x: Type.Number() })]) + const T = Type.Intersect([A, B]) + Ok(T, { x: 1 }) + Fail(T, { x: '1' }) + }) + it('Should not intersect two union objects with overlapping properties of varying types', () => { + const A = Type.Union([Type.Object({ x: Type.Number() })]) + const B = Type.Union([Type.Object({ x: Type.String() })]) + const T = Type.Intersect([A, B]) + Fail(T, { x: 1 }) + Fail(T, { x: '1' }) + }) + it('Should intersect two union objects with non-overlapping properties', () => { + const A = Type.Union([Type.Object({ x: Type.Number() })]) + const B = Type.Union([Type.Object({ y: Type.Number() })]) + const T = Type.Intersect([A, B]) + Ok(T, { x: 1, y: 1 }) + }) + it('Should not intersect two union objects with non-overlapping properties for additionalProperties false', () => { + const A = Type.Union([Type.Object({ x: Type.Number() }, { additionalProperties: false })]) + const B = Type.Union([Type.Object({ y: Type.Number() }, { additionalProperties: false })]) + const T = Type.Intersect([A, B]) + Fail(T, { x: 1, y: 1 }) + }) + it('unevaluatedProperties with Record 1', () => { + const T = Type.Intersect( + [ + Type.Record(Type.Number(), Type.String()), + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + ], + { + unevaluatedProperties: false, + }, + ) + Ok(T, { x: 1, y: 2 }) + }) + it('unevaluatedProperties with Record 2', () => { + const T = Type.Intersect( + [ + Type.Record(Type.Number(), Type.String()), + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + ], + { + unevaluatedProperties: false, + }, + ) + Ok(T, { x: 1, y: 2, 0: 'hello' }) + }) + it('unevaluatedProperties with Record 3', () => { + const T = Type.Intersect( + [ + Type.Record(Type.Number(), Type.String()), + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + ], + { + unevaluatedProperties: false, + }, + ) + Fail(T, { x: 1, y: 2, 0: 1 }) + }) + it('unevaluatedProperties with Record 4', () => { + const T = Type.Intersect( + [ + Type.Record(Type.Number(), Type.String()), + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + ], + { + unevaluatedProperties: Type.Boolean(), + }, + ) + Ok(T, { x: 1, y: 2 }) + }) + it('unevaluatedProperties with Record 5', () => { + const T = Type.Intersect( + [ + Type.Record(Type.Number(), Type.String()), + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + ], + { + unevaluatedProperties: Type.Boolean(), + }, + ) + Ok(T, { x: 1, y: 2, z: true }) + }) + it('unevaluatedProperties with Record 6', () => { + const T = Type.Intersect( + [ + Type.Record(Type.Number(), Type.String()), + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + ], + { + unevaluatedProperties: Type.Boolean(), + }, + ) + Fail(T, { x: 1, y: 2, z: 1 }) + }) + it('unevaluatedProperties with Record 7', () => { + const T = Type.Intersect( + [ + Type.Record(Type.Number(), Type.String()), + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + ], + { + unevaluatedProperties: Type.Boolean(), + }, + ) + Ok(T, { x: 1, y: 2, 0: '' }) + }) + it('unevaluatedProperties with Record 8', () => { + const T = Type.Intersect( + [ + Type.Record(Type.Number(), Type.String()), + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + ], + { + unevaluatedProperties: Type.Boolean(), + }, + ) + Ok(T, { x: 1, y: 2, 0: '', z: true }) + }) + it('unevaluatedProperties with Record 9', () => { + const T = Type.Intersect( + [ + Type.Record(Type.Number(), Type.String()), + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + ], + { + unevaluatedProperties: Type.Boolean(), + }, + ) + Fail(T, { x: 1, y: 2, 0: '', z: 1 }) + }) +}) diff --git a/test/runtime/compiler/iterator.ts b/test/runtime/compiler/iterator.ts new file mode 100644 index 000000000..e5c5e0bb1 --- /dev/null +++ b/test/runtime/compiler/iterator.ts @@ -0,0 +1,20 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler/Iterator', () => { + it('Should validate a iterator 1', () => { + function* f() {} + const T = Type.Iterator(Type.Any()) + Ok(T, f()) + }) + it('Should validate a iterator 2', () => { + const T = Type.Iterator(Type.Any()) + Ok(T, { + [Symbol.iterator]: () => {}, + }) + }) + it('Should not validate a iterator', () => { + const T = Type.Iterator(Type.Any()) + Fail(T, {}) + }) +}) diff --git a/test/runtime/compiler/keyof.ts b/test/runtime/compiler/keyof.ts new file mode 100644 index 000000000..a1d5cc458 --- /dev/null +++ b/test/runtime/compiler/keyof.ts @@ -0,0 +1,48 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler/KeyOf', () => { + 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(), + }), + ) + Ok(T, 'x') + Ok(T, 'y') + Ok(T, 'z') + Fail(T, 'w') + }) + it('Should validate when using pick', () => { + const T = Type.KeyOf( + Type.Pick( + Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }), + ['x', 'y'], + ), + ) + Ok(T, 'x') + Ok(T, 'y') + Fail(T, 'z') + }) + it('Should validate when using omit', () => { + const T = Type.KeyOf( + Type.Omit( + Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }), + ['x', 'y'], + ), + ) + Fail(T, 'x') + Fail(T, 'y') + Ok(T, 'z') + }) +}) diff --git a/test/runtime/compiler/kind.ts b/test/runtime/compiler/kind.ts new file mode 100644 index 000000000..1f0274b00 --- /dev/null +++ b/test/runtime/compiler/kind.ts @@ -0,0 +1,123 @@ +import { TypeRegistry, Type, Kind, TSchema } from '@sinclair/typebox' +import { TypeCompiler } from '@sinclair/typebox/compiler' +import { Ok, Fail } from './validate' +import { Assert } from '../assert' + +describe('compiler/Kind', () => { + // ------------------------------------------------------------ + // Fixtures + // ------------------------------------------------------------ + beforeEach(() => TypeRegistry.Set('PI', (_, value) => value === Math.PI)) + afterEach(() => TypeRegistry.Delete('PI')) + // ------------------------------------------------------------ + // Tests + // ------------------------------------------------------------ + it('Should validate', () => { + const T = Type.Unsafe({ [Kind]: 'PI' }) + Ok(T, Math.PI) + }) + it('Should not validate', () => { + const T = Type.Unsafe({ [Kind]: 'PI' }) + Fail(T, Math.PI * 2) + }) + it('Should validate in object', () => { + const T = Type.Object({ + x: Type.Unsafe({ [Kind]: 'PI' }), + }) + Ok(T, { x: Math.PI }) + }) + it('Should not validate in object', () => { + const T = Type.Object({ + x: Type.Unsafe({ [Kind]: 'PI' }), + }) + Fail(T, { x: Math.PI * 2 }) + }) + it('Should validate in array', () => { + const T = Type.Array(Type.Unsafe({ [Kind]: 'PI' })) + Ok(T, [Math.PI]) + }) + it('Should not validate in array', () => { + const T = Type.Array(Type.Unsafe({ [Kind]: 'PI' })) + Fail(T, [Math.PI * 2]) + }) + it('Should validate in tuple', () => { + const T = Type.Tuple([Type.Unsafe({ [Kind]: 'PI' })]) + Ok(T, [Math.PI]) + }) + it('Should not validate in tuple', () => { + const T = Type.Tuple([Type.Unsafe({ [Kind]: 'PI' })]) + Fail(T, [Math.PI * 2]) + }) + // ------------------------------------------------------------ + // Instances + // ------------------------------------------------------------ + it('Should receive kind instance on registry callback', () => { + const stack: string[] = [] + TypeRegistry.Set('Kind', (schema: unknown) => { + // prettier-ignore + return (typeof schema === 'object' && schema !== null && Kind in schema && schema[Kind] === 'Kind' && '$id' in schema && typeof schema.$id === 'string') + ? (() => { stack.push(schema.$id); return true })() + : false + }) + const A = { [Kind]: 'Kind', $id: 'A' } as TSchema + const B = { [Kind]: 'Kind', $id: 'B' } as TSchema + const T = Type.Object({ a: A, b: B }) + const C = TypeCompiler.Compile(T) + const R = C.Check({ a: null, b: null }) + Assert.IsTrue(R) + Assert.IsEqual(stack[0], 'A') + Assert.IsEqual(stack[1], 'B') + TypeRegistry.Delete('Kind') + }) + // ------------------------------------------------------------ + // Instances Retain + // ------------------------------------------------------------ + it('Should retain kind instances on subsequent compile', () => { + let stack: string[] = [] + TypeRegistry.Set('Kind', (schema: unknown) => { + // prettier-ignore + return (typeof schema === 'object' && schema !== null && Kind in schema && schema[Kind] === 'Kind' && '$id' in schema && typeof schema.$id === 'string') + ? (() => { stack.push(schema.$id); return true })() + : false + }) + const A = { [Kind]: 'Kind', $id: 'A' } as TSchema + const B = { [Kind]: 'Kind', $id: 'B' } as TSchema + const C = { [Kind]: 'Kind', $id: 'C' } as TSchema + const D = { [Kind]: 'Kind', $id: 'D' } as TSchema + const T1 = Type.Object({ a: A, b: B }) + const T2 = Type.Object({ a: C, b: D }) + + // Compile T1 and run check, expect A and B + const C1 = TypeCompiler.Compile(T1) + const R1 = C1.Check({ a: null, b: null }) + Assert.IsTrue(R1) + Assert.IsEqual(stack.length, 2) + Assert.IsEqual(stack[0], 'A') + Assert.IsEqual(stack[1], 'B') + stack = [] + // compile T2 and force instance.clear() + const C2 = TypeCompiler.Compile(T2) + // run T1 check + const R2 = C1.Check({ a: null, b: null }) + Assert.IsTrue(R2) + Assert.IsEqual(stack.length, 2) + Assert.IsEqual(stack[0], 'A') + Assert.IsEqual(stack[1], 'B') + stack = [] + // run T2 check + const R3 = C2.Check({ a: null, b: null }) + Assert.IsTrue(R3) + Assert.IsEqual(stack.length, 2) + Assert.IsEqual(stack[0], 'C') + Assert.IsEqual(stack[1], 'D') + stack = [] + // run T1 check + const R4 = C1.Check({ a: null, b: null }) + Assert.IsTrue(R4) + Assert.IsEqual(stack.length, 2) + Assert.IsEqual(stack[0], 'A') + Assert.IsEqual(stack[1], 'B') + stack = [] + TypeRegistry.Delete('Kind') + }) +}) diff --git a/test/runtime/compiler/literal.ts b/test/runtime/compiler/literal.ts new file mode 100644 index 000000000..5cd3e289b --- /dev/null +++ b/test/runtime/compiler/literal.ts @@ -0,0 +1,50 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler/Literal', () => { + 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') + }) + 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') + }) + // reference: https://github.com/sinclairzx81/typebox/issues/539 + it('Should escape single quote literals', () => { + const T = Type.Literal("it's") + Ok(T, "it's") + Fail(T, "it''s") + }) + it('Should escape multiple single quote literals', () => { + const T = Type.Literal("'''''''''") + Ok(T, "'''''''''") + Fail(T, "''''''''") // minus 1 + }) +}) diff --git a/test/runtime/compiler/module.ts b/test/runtime/compiler/module.ts new file mode 100644 index 000000000..2121a9d5d --- /dev/null +++ b/test/runtime/compiler/module.ts @@ -0,0 +1,144 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler/Module', () => { + it('Should validate string', () => { + const Module = Type.Module({ + A: Type.String(), + }) + const T = Module.Import('A') + Ok(T, 'hello') + Fail(T, true) + }) + it('Should validate referenced string', () => { + const Module = Type.Module({ + A: Type.String(), + B: Type.Ref('A'), + }) + const T = Module.Import('B') + Ok(T, 'hello') + Fail(T, true) + }) + it('Should validate self referential', () => { + const Module = Type.Module({ + A: Type.Object({ + nodes: Type.Array(Type.Ref('A')), + }), + }) + const T = Module.Import('A') + Ok(T, { nodes: [{ nodes: [{ nodes: [] }, { nodes: [] }] }] }) + Fail(T, { nodes: [{ nodes: [{ nodes: [] }, { nodes: false }] }] }) + Fail(T, true) + }) + it('Should validate mutual recursive', () => { + const Module = Type.Module({ + A: Type.Object({ + b: Type.Ref('B'), + }), + B: Type.Object({ + a: Type.Union([Type.Ref('A'), Type.Null()]), + }), + }) + const T = Module.Import('A') + Ok(T, { b: { a: null } }) + Ok(T, { b: { a: { b: { a: null } } } }) + Fail(T, { b: { a: 1 } }) + Fail(T, { b: { a: { b: { a: 1 } } } }) + Fail(T, true) + }) + it('Should validate mutual recursive (Array)', () => { + const Module = Type.Module({ + A: Type.Object({ + b: Type.Ref('B'), + }), + B: Type.Object({ + a: Type.Array(Type.Ref('A')), + }), + }) + const T = Module.Import('A') + Ok(T, { b: { a: [{ b: { a: [] } }] } }) + Fail(T, { b: { a: [{ b: { a: [null] } }] } }) + Fail(T, true) + }) + it('Should validate deep referential', () => { + const Module = Type.Module({ + A: Type.Ref('B'), + B: Type.Ref('C'), + C: Type.Ref('D'), + D: Type.Ref('E'), + E: Type.Ref('F'), + F: Type.Ref('G'), + G: Type.Ref('H'), + H: Type.Literal('hello'), + }) + const T = Module.Import('A') + Ok(T, 'hello') + Fail(T, 'world') + }) + // ---------------------------------------------------------------- + // Modifiers + // ---------------------------------------------------------------- + it('Should validate objects with property modifiers 1', () => { + const Module = Type.Module({ + T: Type.Object({ + x: Type.ReadonlyOptional(Type.Null()), + y: Type.Readonly(Type.Null()), + z: Type.Optional(Type.Null()), + w: Type.Null(), + }), + }) + const T = Module.Import('T') + Ok(T, { x: null, y: null, w: null }) + Ok(T, { y: null, w: null }) + Fail(T, { x: 1, y: null, w: null }) + }) + it('Should validate objects with property modifiers 2', () => { + const Module = Type.Module({ + T: Type.Object({ + x: Type.ReadonlyOptional(Type.Array(Type.Null())), + y: Type.Readonly(Type.Array(Type.Null())), + z: Type.Optional(Type.Array(Type.Null())), + w: Type.Array(Type.Null()), + }), + }) + const T = Module.Import('T') + Ok(T, { x: [null], y: [null], w: [null] }) + Ok(T, { y: [null], w: [null] }) + Fail(T, { x: [1], y: [null], w: [null] }) + }) + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/1109 + // ---------------------------------------------------------------- + it('Should validate deep referential 1', () => { + const Module = Type.Module({ + A: Type.Union([Type.Literal('Foo'), Type.Literal('Bar')]), + B: Type.Ref('A'), + C: Type.Object({ ref: Type.Ref('B') }), + D: Type.Union([Type.Ref('B'), Type.Ref('C')]), + }) + Ok(Module.Import('A') as never, 'Foo') + Ok(Module.Import('A') as never, 'Bar') + Ok(Module.Import('B') as never, 'Foo') + Ok(Module.Import('B') as never, 'Bar') + Ok(Module.Import('C') as never, { ref: 'Foo' }) + Ok(Module.Import('C') as never, { ref: 'Bar' }) + Ok(Module.Import('D') as never, 'Foo') + Ok(Module.Import('D') as never, 'Bar') + Ok(Module.Import('D') as never, { ref: 'Foo' }) + Ok(Module.Import('D') as never, { ref: 'Bar' }) + }) + it('Should validate deep referential 2', () => { + const Module = Type.Module({ + A: Type.Literal('Foo'), + B: Type.Ref('A'), + C: Type.Ref('B'), + D: Type.Ref('C'), + E: Type.Ref('D'), + }) + Ok(Module.Import('A'), 'Foo') + Ok(Module.Import('B'), 'Foo') + Ok(Module.Import('C'), 'Foo') + Ok(Module.Import('D'), 'Foo') + Ok(Module.Import('E'), 'Foo') + }) +}) diff --git a/test/runtime/compiler/never.ts b/test/runtime/compiler/never.ts new file mode 100644 index 000000000..e1a8c3afc --- /dev/null +++ b/test/runtime/compiler/never.ts @@ -0,0 +1,41 @@ +import { Type } from '@sinclair/typebox' +import { Fail } from './validate' + +describe('compiler/Never', () => { + it('Should not validate number', () => { + const T = Type.Never() + Fail(T, 1) + }) + it('Should not validate string', () => { + const T = Type.Never() + Fail(T, 'hello') + }) + it('Should not validate boolean', () => { + const T = Type.Never() + Fail(T, true) + }) + it('Should not validate array', () => { + const T = Type.Never() + Fail(T, [1, 2, 3]) + }) + it('Should not validate object', () => { + const T = Type.Never() + Fail(T, { a: 1, b: 2 }) + }) + it('Should not validate null', () => { + const T = Type.Never() + Fail(T, null) + }) + it('Should not validate undefined', () => { + const T = Type.Never() + Fail(T, undefined) + }) + it('Should not validate bigint', () => { + const T = Type.Never() + Fail(T, BigInt(1)) + }) + it('Should not validate symbol', () => { + const T = Type.Never() + Fail(T, Symbol(1)) + }) +}) diff --git a/test/runtime/compiler/not.ts b/test/runtime/compiler/not.ts new file mode 100644 index 000000000..3621e91c4 --- /dev/null +++ b/test/runtime/compiler/not.ts @@ -0,0 +1,45 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler/Not', () => { + it('Should validate not number', () => { + const T = Type.Not(Type.Number()) + Fail(T, 1) + Ok(T, '1') + }) + it('Should validate not not number', () => { + const T = Type.Not(Type.Not(Type.Number())) + Ok(T, 1) + Fail(T, '1') + }) + it('Should validate not union', () => { + // prettier-ignore + const T = Type.Not(Type.Union([ + Type.Literal('A'), + Type.Literal('B'), + Type.Literal('C') + ])) + Fail(T, 'A') + Fail(T, 'B') + Fail(T, 'C') + Ok(T, 'D') + }) + it('Should validate not object intersection', () => { + const T = Type.Intersect([ + Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }), + Type.Object({ + x: Type.Not(Type.Literal(0)), + y: Type.Not(Type.Literal(0)), + z: Type.Not(Type.Literal(0)), + }), + ]) + Fail(T, { x: 0, y: 0, z: 0 }) + Fail(T, { x: 1, y: 0, z: 0 }) + Fail(T, { x: 1, y: 1, z: 0 }) + Ok(T, { x: 1, y: 1, z: 1 }) + }) +}) diff --git a/test/runtime/compiler/null.ts b/test/runtime/compiler/null.ts new file mode 100644 index 000000000..b23335f0b --- /dev/null +++ b/test/runtime/compiler/null.ts @@ -0,0 +1,41 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler/Null', () => { + 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) + }) + it('Should not validate bigint', () => { + const T = Type.Null() + Fail(T, BigInt(1)) + }) + it('Should not validate symbol', () => { + const T = Type.Null() + Fail(T, Symbol(1)) + }) +}) diff --git a/test/runtime/compiler/number.ts b/test/runtime/compiler/number.ts new file mode 100644 index 000000000..7a97df5f5 --- /dev/null +++ b/test/runtime/compiler/number.ts @@ -0,0 +1,77 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler/Number', () => { + it('Should validate number', () => { + const T = Type.Number() + Ok(T, 3.14) + }) + it('Should not validate NaN', () => { + const T = Type.Number() + Fail(T, NaN) + }) + it('Should not validate +Infinity', () => { + const T = Type.Number() + Fail(T, Infinity) + }) + it('Should not validate -Infinity', () => { + const T = Type.Number() + Fail(T, -Infinity) + }) + it('Should validate integer', () => { + 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) + }) + it('Should not validate bigint', () => { + const T = Type.Number() + Fail(T, BigInt(1)) + }) + it('Should not validate symbol', () => { + const T = Type.Number() + Fail(T, Symbol(1)) + }) + it('Should validate minimum', () => { + const T = Type.Number({ minimum: 10 }) + Fail(T, 9) + Ok(T, 10) + }) + it('Should validate maximum', () => { + const T = Type.Number({ maximum: 10 }) + Ok(T, 10) + Fail(T, 11) + }) + it('Should validate Date exclusiveMinimum', () => { + const T = Type.Number({ exclusiveMinimum: 10 }) + Fail(T, 10) + Ok(T, 11) + }) + it('Should validate Date exclusiveMaximum', () => { + const T = Type.Number({ exclusiveMaximum: 10 }) + Ok(T, 9) + Fail(T, 10) + }) +}) diff --git a/test/runtime/compiler/object.ts b/test/runtime/compiler/object.ts new file mode 100644 index 000000000..10c154b4b --- /dev/null +++ b/test/runtime/compiler/object.ts @@ -0,0 +1,390 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler/Object', () => { + // ----------------------------------------------------- + // TypeCompiler Only + // ----------------------------------------------------- + it('Should handle extends undefined check 1', () => { + const T = Type.Object({ + A: Type.Not(Type.Number()), + B: Type.Union([Type.Number(), Type.Undefined()]), + C: Type.Intersect([Type.Undefined(), Type.Undefined()]), + }) + Ok(T, { + A: undefined, + B: undefined, + C: undefined, + }) + }) + // https://github.com/sinclairzx81/typebox/issues/437 + it('Should handle extends undefined check 2', () => { + const T = Type.Object({ + A: Type.Not(Type.Null()), + }) + Ok(T, { A: undefined }) + Fail(T, { A: null }) + Fail(T, {}) + }) + // ----------------------------------------------------- + // Standard Checks + // ----------------------------------------------------- + it('Should not validate a number', () => { + const T = Type.Object({}) + Fail(T, 42) + }) + it('Should not validate a string', () => { + const T = Type.Object({}) + Fail(T, 'hello') + }) + it('Should not validate a boolean', () => { + const T = Type.Object({}) + Fail(T, true) + }) + it('Should not validate a null', () => { + const T = Type.Object({}) + Fail(T, null) + }) + it('Should not validate undefined', () => { + const T = Type.Object({}) + Fail(T, undefined) + }) + it('Should not validate bigint', () => { + const T = Type.Object({}) + Fail(T, BigInt(1)) + }) + it('Should not validate symbol', () => { + const T = Type.Object({}) + Fail(T, Symbol(1)) + }) + it('Should not validate an array', () => { + const T = Type.Object({}) + Fail(T, [1, 2]) + }) + 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 }, + }) + }) + it('Should allow additionalProperties by default', () => { + const T = Type.Object({ + a: Type.Number(), + b: Type.String(), + }) + Ok(T, { + a: 1, + b: 'hello', + c: true, + }) + }) + it('Should not allow an empty object if minProperties is set to 1', () => { + const T = Type.Object( + { + a: Type.Optional(Type.Number()), + b: Type.Optional(Type.String()), + }, + { additionalProperties: false, minProperties: 1 }, + ) + Ok(T, { a: 1 }) + Ok(T, { b: 'hello' }) + Fail(T, {}) + }) + it('Should not allow 3 properties if maxProperties is set to 2', () => { + const T = Type.Object( + { + a: Type.Optional(Type.Number()), + b: Type.Optional(Type.String()), + c: Type.Optional(Type.Boolean()), + }, + { additionalProperties: false, maxProperties: 2 }, + ) + Ok(T, { a: 1 }) + Ok(T, { a: 1, b: 'hello' }) + Fail(T, { + a: 1, + b: 'hello', + c: true, + }) + }) + 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, + }) + }) + it('Should not allow properties for an empty object when additionalProperties is false', () => { + const T = Type.Object({}, { additionalProperties: false }) + Ok(T, {}) + Fail(T, { a: 10 }) + }) + + it('Should validate with non-syntax property keys', () => { + const T = Type.Object({ + 'with-hyphen': Type.Literal(1), + '0-leading': Type.Literal(2), + '$-leading': Type.Literal(3), + '!@#$%^&*(': Type.Literal(4), + 'node-mirror:release:0': Type.Literal(5), // issue: 353 + 'node-mirror:release:1': Type.Optional(Type.Literal(6)), // issue: 356 + 'node-mirror:release:2': Type.Union([Type.Literal(7), Type.Undefined()]), // key known + "a'a": Type.Literal(8), + '@onlyAtSymbol': Type.Literal(9), + }) + Ok(T, { + 'with-hyphen': 1, + '0-leading': 2, + '$-leading': 3, + '!@#$%^&*(': 4, + 'node-mirror:release:0': 5, + 'node-mirror:release:1': 6, + 'node-mirror:release:2': 7, + "a'a": 8, + '@onlyAtSymbol': 9, + }) + }) + it('Should validate schema additional properties of string', () => { + const T = Type.Object( + { + x: Type.Number(), + y: Type.Number(), + }, + { + additionalProperties: Type.String(), + }, + ) + Ok(T, { + x: 1, + y: 2, + z: 'hello', + }) + Fail(T, { + x: 1, + y: 2, + z: 3, + }) + }) + it('Should validate schema additional properties of array', () => { + const T = Type.Object( + { + x: Type.Number(), + y: Type.Number(), + }, + { + additionalProperties: Type.Array(Type.Number()), + }, + ) + Ok(T, { + x: 1, + y: 2, + z: [0, 1, 2], + }) + Fail(T, { + x: 1, + y: 2, + z: 3, + }) + }) + it('Should validate schema additional properties of object', () => { + const T = Type.Object( + { + x: Type.Number(), + y: Type.Number(), + }, + { + additionalProperties: Type.Object({ + z: Type.Number(), + }), + }, + ) + Ok(T, { + x: 1, + y: 2, + z: { z: 1 }, + }) + Fail(T, { + x: 1, + y: 2, + z: 3, + }) + }) + it('Should validate nested schema additional properties of string', () => { + const T = Type.Object({ + nested: Type.Object( + { + x: Type.Number(), + y: Type.Number(), + }, + { + additionalProperties: Type.String(), + }, + ), + }) + Ok(T, { + nested: { + x: 1, + y: 2, + z: 'hello', + }, + }) + Fail(T, { + nested: { + x: 1, + y: 2, + z: 3, + }, + }) + }) + it('Should validate nested schema additional properties of array', () => { + const T = Type.Object({ + nested: Type.Object( + { + x: Type.Number(), + y: Type.Number(), + }, + { + additionalProperties: Type.Array(Type.Number()), + }, + ), + }) + Ok(T, { + nested: { + x: 1, + y: 2, + z: [0, 1, 2], + }, + }) + Fail(T, { + nested: { + x: 1, + y: 2, + z: 3, + }, + }) + }) + it('Should validate nested schema additional properties of object', () => { + const T = Type.Object({ + nested: Type.Object( + { + x: Type.Number(), + y: Type.Number(), + }, + { + additionalProperties: Type.Object({ + z: Type.Number(), + }), + }, + ), + }) + Ok(T, { + nested: { + x: 1, + y: 2, + z: { z: 1 }, + }, + }) + Fail(T, { + nested: { + x: 1, + y: 2, + z: 3, + }, + }) + }) + it('Should check for property key if property type is undefined', () => { + const T = Type.Object({ x: Type.Undefined() }) + Ok(T, { x: undefined }) + Fail(T, {}) + }) + it('Should check for property key if property type extends undefined', () => { + const T = Type.Object({ x: Type.Union([Type.Number(), Type.Undefined()]) }) + Ok(T, { x: 1 }) + Ok(T, { x: undefined }) + Fail(T, {}) + }) + it('Should not check for property key if property type is undefined and optional', () => { + const T = Type.Object({ x: Type.Optional(Type.Undefined()) }) + Ok(T, { x: undefined }) + Ok(T, {}) + }) + it('Should not check for property key if property type extends undefined and optional', () => { + const T = Type.Object({ x: Type.Optional(Type.Union([Type.Number(), Type.Undefined()])) }) + Ok(T, { x: 1 }) + Ok(T, { x: undefined }) + Ok(T, {}) + }) + it('Should check undefined for optional property of number', () => { + const T = Type.Object({ x: Type.Optional(Type.Number()) }) + Ok(T, { x: 1 }) + Ok(T, { x: undefined }) // allowed by default + Ok(T, {}) + }) + it('Should check undefined for optional property of undefined', () => { + const T = Type.Object({ x: Type.Optional(Type.Undefined()) }) + Fail(T, { x: 1 }) + Ok(T, { x: undefined }) + Ok(T, {}) + }) + it('Should check for required property of any', () => { + const T = Type.Object({ x: Type.Any() }) + Fail(T, {}) + Ok(T, { x: undefined }) + Ok(T, { x: 1 }) + Ok(T, { x: true }) + }) + it('Should check for required property of unknown', () => { + const T = Type.Object({ x: Type.Unknown() }) + Fail(T, {}) + Ok(T, { x: undefined }) + Ok(T, { x: 1 }) + Ok(T, { x: true }) + }) + it('Should check for required property of any (when optional)', () => { + const T = Type.Object({ x: Type.Optional(Type.Any()) }) + Ok(T, {}) + Ok(T, { x: undefined }) + Ok(T, { x: 1 }) + Ok(T, { x: true }) + }) + it('Should check for required property of unknown (when optional)', () => { + const T = Type.Object({ x: Type.Optional(Type.Unknown()) }) + Ok(T, {}) + Ok(T, { x: undefined }) + Ok(T, { x: 1 }) + Ok(T, { x: true }) + }) +}) diff --git a/test/runtime/compiler/omit.ts b/test/runtime/compiler/omit.ts new file mode 100644 index 000000000..2adf42ed0 --- /dev/null +++ b/test/runtime/compiler/omit.ts @@ -0,0 +1,83 @@ +import { Type, Kind } from '@sinclair/typebox' +import { Ok, Fail } from './validate' +import { deepEqual, strictEqual } from 'assert' + +describe('compiler/Omit', () => { + it('Should omit properties on the source schema', () => { + const A = Type.Object( + { + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }, + { 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) + }) + it('Should omit with keyof object', () => { + const A = Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }) + const B = Type.Object({ + x: Type.Number(), + y: Type.Number(), + }) + const T = Type.Omit(A, Type.KeyOf(B), { additionalProperties: false }) + Ok(T, { z: 0 }) + Fail(T, { x: 0, y: 0, z: 0 }) + }) + it('Should support Omit of Literal', () => { + const A = Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }) + const T = Type.Omit(A, Type.Literal('x'), { + additionalProperties: false, + }) + Ok(T, { y: 1, z: 1 }) + Fail(T, { x: 1, y: 1, z: 1 }) + }) + it('Should support Omit of Never', () => { + const A = Type.Object( + { + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }, + { additionalProperties: false }, + ) + const T = Type.Omit(A, Type.Never()) + Fail(T, { y: 1, z: 1 }) + Ok(T, { x: 1, y: 1, z: 1 }) + }) +}) diff --git a/test/runtime/compiler/optional.ts b/test/runtime/compiler/optional.ts new file mode 100644 index 000000000..5c24e175c --- /dev/null +++ b/test/runtime/compiler/optional.ts @@ -0,0 +1,27 @@ +import { strictEqual } from 'assert' +import { Type } from '@sinclair/typebox' +import { Ok } from './validate' + +describe('compiler/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/test/runtime/compiler/partial.ts b/test/runtime/compiler/partial.ts new file mode 100644 index 000000000..74b71aa60 --- /dev/null +++ b/test/runtime/compiler/partial.ts @@ -0,0 +1,53 @@ +import { TypeSystem } from '@sinclair/typebox/system' +import { Type, OptionalKind, ReadonlyKind } from '@sinclair/typebox' +import { Ok, Fail } from './validate' +import { strictEqual } from 'assert' + +describe('compiler/Partial', () => { + it('Should convert a required object into a partial', () => { + const A = Type.Object( + { + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }, + { 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('Should update modifier types correctly when converting to partial', () => { + const A = Type.Object( + { + x: Type.Readonly(Type.Optional(Type.Number())), + y: Type.Readonly(Type.Number()), + z: Type.Optional(Type.Number()), + w: Type.Number(), + }, + { additionalProperties: false }, + ) + const T = Type.Partial(A) + strictEqual(T.properties.x[ReadonlyKind], 'Readonly') + strictEqual(T.properties.x[OptionalKind], 'Optional') + strictEqual(T.properties.y[ReadonlyKind], 'Readonly') + strictEqual(T.properties.y[OptionalKind], 'Optional') + strictEqual(T.properties.z[OptionalKind], 'Optional') + strictEqual(T.properties.w[OptionalKind], 'Optional') + }) + 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.Partial(A) + strictEqual(A.additionalProperties, false) + strictEqual(T.additionalProperties, false) + }) +}) diff --git a/test/runtime/compiler/pick.ts b/test/runtime/compiler/pick.ts new file mode 100644 index 000000000..59915e257 --- /dev/null +++ b/test/runtime/compiler/pick.ts @@ -0,0 +1,82 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' +import { strictEqual } from 'assert' + +describe('compiler/Pick', () => { + it('Should pick properties from the source schema', () => { + const Vector3 = Type.Object( + { + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }, + { 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) + }) + + it('Should pick with keyof object', () => { + const A = Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }) + const B = Type.Object({ + x: Type.Number(), + y: Type.Number(), + }) + const T = Type.Pick(A, Type.KeyOf(B), { additionalProperties: false }) + Ok(T, { x: 0, y: 0 }) + Fail(T, { x: 0, y: 0, z: 0 }) + }) + it('Should support Pick of Literal', () => { + const A = Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }) + const T = Type.Pick(A, Type.Literal('x'), { + additionalProperties: false, + }) + Ok(T, { x: 1 }) + Fail(T, { x: 1, y: 1, z: 1 }) + }) + it('Should support Pick of Never', () => { + const A = Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }) + const T = Type.Pick(A, Type.Never(), { + additionalProperties: false, + }) + Fail(T, { x: 1, y: 1, z: 1 }) + Ok(T, {}) + }) +}) diff --git a/test/runtime/compiler/readonly-optional.ts b/test/runtime/compiler/readonly-optional.ts new file mode 100644 index 000000000..e4aeffc88 --- /dev/null +++ b/test/runtime/compiler/readonly-optional.ts @@ -0,0 +1,27 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' +import { strictEqual } from 'assert' + +describe('compiler/ReadonlyOptional', () => { + it('Should validate object with optional', () => { + const T = Type.Object( + { + a: Type.Readonly(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.Readonly(Type.Optional(Type.String())), + b: Type.String(), + }, + { additionalProperties: false }, + ) + strictEqual(T.required!.includes('a'), false) + }) +}) diff --git a/test/runtime/compiler/readonly.ts b/test/runtime/compiler/readonly.ts new file mode 100644 index 000000000..b852feb44 --- /dev/null +++ b/test/runtime/compiler/readonly.ts @@ -0,0 +1,27 @@ +import { deepStrictEqual, strictEqual } from 'assert' +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler/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) + }) +}) diff --git a/test/runtime/compiler/record.ts b/test/runtime/compiler/record.ts new file mode 100644 index 000000000..af4417ba8 --- /dev/null +++ b/test/runtime/compiler/record.ts @@ -0,0 +1,324 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler/Record', () => { + // ------------------------------------------------------------- + // Issues + // ------------------------------------------------------------- + it('Issue: https://github.com/sinclairzx81/typebox/issues/402', () => { + const T = Type.Object({ + foo: Type.Object({ + bar: Type.Record(Type.String(), Type.Number()), + }), + }) + Ok(T, { foo: { bar: { x: 42 } } }) + Ok(T, { foo: { bar: {} } }) + Fail(T, { foo: { bar: { x: '42' } } }) + Fail(T, { foo: { bar: [] } }) + Fail(T, { foo: {} }) + Fail(T, { foo: [] }) + Fail(T, {}) + }) + // ------------------------------------------------------------- + // TypeBox Only: Date and Record + // ------------------------------------------------------------- + it('Should fail record with Date', () => { + const T = Type.Record(Type.String(), Type.String()) + Fail(T, new Date()) + }) + it('Should fail record with Uint8Array', () => { + const T = Type.Record(Type.String(), Type.String()) + Fail(T, new Uint8Array()) + }) + // ------------------------------------------------------------- + // Standard Assertions + // ------------------------------------------------------------- + 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 not validate when below minProperties', () => { + const T = Type.Record(Type.String(), Type.Number(), { minProperties: 4 }) + Ok(T, { a: 1, b: 2, c: 3, d: 4 }) + Fail(T, { a: 1, b: 2, c: 3 }) + }) + it('Should not validate when above maxProperties', () => { + const T = Type.Record(Type.String(), Type.Number(), { maxProperties: 4 }) + Ok(T, { a: 1, b: 2, c: 3, d: 4 }) + Fail(T, { a: 1, b: 2, c: 3, d: 4, e: 5 }) + }) + it('Should not validate with illogical minProperties | maxProperties', () => { + const T = Type.Record(Type.String(), Type.Number(), { minProperties: 5, maxProperties: 4 }) + Fail(T, { a: 1, b: 2, c: 3 }) + Fail(T, { a: 1, b: 2, c: 3, d: 4 }) + Fail(T, { a: 1, b: 2, c: 3, d: 4, e: 5 }) + }) + it('Should validate when specifying string union literals when additionalProperties is true', () => { + 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 string union literals when additionalProperties is 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 validate for keyof records', () => { + const T = Type.Object({ + a: Type.String(), + b: Type.Number(), + c: Type.String(), + }) + const R = Type.Record(Type.KeyOf(T), Type.Number()) + Ok(R, { a: 1, b: 2, c: 3 }) + }) + it('Should not validate for unknown key via keyof', () => { + const T = Type.Object({ + a: Type.String(), + b: Type.Number(), + c: Type.String(), + }) + const R = Type.Record(Type.KeyOf(T), Type.Number(), { additionalProperties: false }) + Fail(R, { a: 1, b: 2, c: 3, d: 4 }) + }) + it('Should validate when specifying regular expressions', () => { + const K = Type.RegExp(/^op_.*$/) + const T = Type.Record(K, Type.Number()) + Ok(T, { + op_a: 1, + op_b: 2, + op_c: 3, + }) + }) + it('Should not validate when specifying regular expressions and passing invalid property', () => { + const K = Type.RegExp(/^op_.*$/) + const T = Type.Record(K, Type.Number(), { additionalProperties: false }) + Fail(T, { + op_a: 1, + op_b: 2, + aop_c: 3, + }) + }) + it('Should validate with quoted string pattern', () => { + const K = Type.String({ pattern: "'(a|b|c)" }) + const T = Type.Record(K, Type.Number()) + Ok(T, { + "'a": 1, + "'b": 2, + "'c": 3, + }) + }) + it('Should validate with forward-slash pattern', () => { + const K = Type.String({ pattern: '/(a|b|c)' }) + const T = Type.Record(K, Type.Number()) + Ok(T, { + '/a': 1, + '/b': 2, + '/c': 3, + }) + }) + // ------------------------------------------------------------ + // Integer Keys + // ------------------------------------------------------------ + it('Should validate when all property keys are integers', () => { + const T = Type.Record(Type.Integer(), Type.Number()) + Ok(T, { '0': 1, '1': 2, '2': 3, '3': 4 }) + }) + it('Should validate when all property keys are integers, but one property is a string with varying type', () => { + const T = Type.Record(Type.Integer(), Type.Number(), { additionalProperties: false }) + Fail(T, { '0': 1, '1': 2, '2': 3, '3': 4, a: 'hello' }) + }) + it('Should not validate if passing a leading zeros for integers keys', () => { + const T = Type.Record(Type.Integer(), Type.Number(), { additionalProperties: false }) + Fail(T, { + '00': 1, + '01': 2, + '02': 3, + '03': 4, + }) + }) + it('Should not validate if passing a signed integers keys', () => { + const T = Type.Record(Type.Integer(), Type.Number(), { additionalProperties: false }) + Fail(T, { + '-0': 1, + '-1': 2, + '-2': 3, + '-3': 4, + }) + }) + // ------------------------------------------------------------ + // Number Keys + // ------------------------------------------------------------ + 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(), { additionalProperties: false }) + Fail(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', () => { + const T = Type.Record(Type.Number(), Type.Number(), { additionalProperties: false }) + Fail(T, { '0': 1, '1': 2, '2': 3, '3': 4, a: 'hello' }) + }) + // ------------------------------------------------------------ + // AdditionalProperties + // ------------------------------------------------------------ + it('AdditionalProperties 1', () => { + const T = Type.Record(Type.Number(), Type.String(), { additionalProperties: true }) + Ok(T, { 1: '', 2: '', x: 1, y: 2, z: 3 }) + }) + it('AdditionalProperties 2', () => { + const T = Type.Record(Type.Number(), Type.String(), { additionalProperties: false }) + Ok(T, { 1: '', 2: '', 3: '' }) + }) + it('AdditionalProperties 3', () => { + const T = Type.Record(Type.Number(), Type.String(), { additionalProperties: false }) + Fail(T, { 1: '', 2: '', x: '' }) + }) + it('AdditionalProperties 4', () => { + const T = Type.Record(Type.Number(), Type.String(), { additionalProperties: Type.Boolean() }) + Fail(T, { 1: '', 2: '', x: '' }) + }) + it('AdditionalProperties 5', () => { + const T = Type.Record(Type.Number(), Type.String(), { additionalProperties: Type.Boolean() }) + Ok(T, { 1: '', 2: '', x: true }) + }) + // ---------------------------------------------------------------- + // TemplateLiteral + // ---------------------------------------------------------------- + it('TemplateLiteral 1', () => { + const K = Type.TemplateLiteral('key${number}') + const R = Type.Record(K, Type.Number(), { additionalProperties: false }) + Ok(R, { + key0: 1, + key1: 1, + key2: 1, + }) + }) + it('TemplateLiteral 2', () => { + const K = Type.TemplateLiteral('key${number}') + const R = Type.Record(K, Type.Number()) + Ok(R, { keyA: 0 }) + }) + it('TemplateLiteral 3', () => { + const K = Type.TemplateLiteral('key${number}') + const R = Type.Record(K, Type.Number(), { additionalProperties: false }) + Fail(R, { keyA: 0 }) + }) + it('TemplateLiteral 4', () => { + const K = Type.TemplateLiteral('key${number}') + const R = Type.Record(K, Type.Number()) + const T = Type.Object({ x: Type.Number(), y: Type.Number() }) + const I = Type.Intersect([R, T], { unevaluatedProperties: false }) + Ok(I, { + x: 1, + y: 2, + key0: 1, + key1: 1, + key2: 1, + }) + }) + it('TemplateLiteral 5', () => { + const K = Type.TemplateLiteral('key${number}') + const R = Type.Record(K, Type.Number()) + const T = Type.Object({ x: Type.Number(), y: Type.Number() }) + const I = Type.Intersect([R, T]) + Ok(I, { + x: 1, + y: 2, + z: 3, + key0: 1, + key1: 1, + key2: 1, + }) + }) + it('TemplateLiteral 6', () => { + const K = Type.TemplateLiteral('key${number}') + const R = Type.Record(K, Type.Number()) + const T = Type.Object({ x: Type.Number(), y: Type.Number() }) + const I = Type.Intersect([R, T], { unevaluatedProperties: false }) + Fail(I, { + x: 1, + y: 2, + z: 3, + key0: 1, + key1: 1, + key2: 1, + }) + }) + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/916 + // ---------------------------------------------------------------- + it('Should validate for string keys', () => { + const T = Type.Record(Type.String(), Type.Null(), { + additionalProperties: false, + }) + Ok(T, { + a: null, + b: null, + 0: null, + 1: null, + }) + }) + it('Should validate for number keys', () => { + const T = Type.Record(Type.Number(), Type.Null(), { + additionalProperties: false, + }) + Fail(T, { + a: null, + b: null, + 0: null, + 1: null, + }) + Ok(T, { + 0: null, + 1: null, + }) + }) + it('Should validate for any keys', () => { + const T = Type.Record(Type.Any(), Type.Null(), { + additionalProperties: false, + }) + Ok(T, { + a: null, + b: null, + 0: null, + 1: null, + }) + }) + it('Should validate for never keys', () => { + const T = Type.Record(Type.Never(), Type.Null(), { + additionalProperties: false, + }) + Ok(T, {}) + Fail(T, { + a: null, + b: null, + 0: null, + 1: null, + }) + }) +}) diff --git a/test/runtime/compiler/recursive.ts b/test/runtime/compiler/recursive.ts new file mode 100644 index 000000000..0b537eca3 --- /dev/null +++ b/test/runtime/compiler/recursive.ts @@ -0,0 +1,79 @@ +import { Type } from '@sinclair/typebox' +import { Assert } from '../assert/index' +import { Ok, Fail } from './validate' + +describe('compiler/Recursive', () => { + it('Should generate default ordinal $id if not specified', () => { + const Node = Type.Recursive((Node) => + Type.Object({ + id: Type.String(), + nodes: Type.Array(Node), + }), + ) + Assert.IsEqual(Node.$id === undefined, false) + }) + it('Should override default ordinal $id if specified', () => { + const Node = Type.Recursive( + (Node) => + Type.Object({ + id: Type.String(), + nodes: Type.Array(Node), + }), + { $id: 'Node' }, + ) + Assert.IsEqual(Node.$id === 'Node', true) + }) + it('Should validate recursive node type', () => { + const Node = Type.Recursive((This) => + Type.Object({ + id: Type.String(), + nodes: Type.Array(This), + }), + ) + Ok(Node, { + id: 'A', + nodes: [ + { id: 'B', nodes: [] }, + { id: 'C', nodes: [] }, + ], + }) + }) + it('Should validate wrapped recursive node type', () => { + const Node = Type.Tuple([ + Type.Recursive((This) => + Type.Object({ + id: Type.String(), + nodes: Type.Array(This), + }), + ), + ]) + Ok(Node, [ + { + id: 'A', + nodes: [ + { id: 'B', nodes: [] }, + { id: 'C', nodes: [] }, + ], + }, + ]) + }) + it('Should not validate wrapped recursive node type with invalid id', () => { + const Node = Type.Tuple([ + Type.Recursive((This) => + Type.Object({ + id: Type.String(), + nodes: Type.Array(This), + }), + ), + ]) + Fail(Node, [ + { + id: 'A', + nodes: [ + { id: 1, nodes: [] }, + { id: 'C', nodes: [] }, + ], + }, + ]) + }) +}) diff --git a/test/runtime/compiler/ref.ts b/test/runtime/compiler/ref.ts new file mode 100644 index 000000000..75470f8e5 --- /dev/null +++ b/test/runtime/compiler/ref.ts @@ -0,0 +1,89 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' +import { Assert } from '../assert/index' + +describe('compiler/Ref', () => { + // ---------------------------------------------------------------- + // Deprecated + // ---------------------------------------------------------------- + it('Should validate for Ref(Schema)', () => { + const T = Type.Number({ $id: 'T' }) + const R = Type.Ref(T) + Ok(R, 1234, [T]) + Fail(R, 'hello', [T]) + }) + // ---------------------------------------------------------------- + // Standard + // ---------------------------------------------------------------- + it('Should should validate when referencing a type', () => { + const T = Type.Object( + { + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }, + { $id: Assert.NextId() }, + ) + const R = Type.Ref(T.$id!) + Ok( + R, + { + x: 1, + y: 2, + z: 3, + }, + [T], + ) + }) + it('Should not validate when passing invalid data', () => { + const T = Type.Object( + { + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }, + { $id: Assert.NextId() }, + ) + const R = Type.Ref(T.$id!) + Fail( + R, + { + x: 1, + y: 2, + }, + [T], + ) + }) + it('Should de-reference object property schema', () => { + const T = Type.Object( + { + name: Type.String(), + }, + { $id: 'R' }, + ) + const R = Type.Object( + { + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + r: Type.Optional(Type.Ref(T.$id!)), + }, + { $id: 'T' }, + ) + Ok(R, { x: 1, y: 2, z: 3 }, [T]) + Ok(R, { x: 1, y: 2, z: 3, r: { name: 'hello' } }, [T]) + Fail(R, { x: 1, y: 2, z: 3, r: { name: 1 } }, [T]) + Fail(R, { x: 1, y: 2, z: 3, r: {} }, [T]) + }) + it('Should reference recursive schema', () => { + const T = Type.Recursive((Node) => + Type.Object({ + id: Type.String(), + nodes: Type.Array(Node), + }), + ) + const R = Type.Ref(T.$id!) + Ok(R, { id: '', nodes: [{ id: '', nodes: [] }] }, [T]) + Fail(R, { id: '', nodes: [{ id: 1, nodes: [] }] }, [T]) + }) +}) diff --git a/test/runtime/compiler/regexp.ts b/test/runtime/compiler/regexp.ts new file mode 100644 index 000000000..d4dda52e6 --- /dev/null +++ b/test/runtime/compiler/regexp.ts @@ -0,0 +1,30 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler/RegExp', () => { + it('Should validate regular expression 1', () => { + const T = Type.RegExp(/foo/i) + Ok(T, 'foo') + Ok(T, 'Foo') + Ok(T, 'fOO') + Fail(T, 'bar') + }) + it('Should validate regular expression 2', () => { + const T = Type.RegExp(/|\p{Extended_Pictographic}/gu) + Ok(T, '♥️♦️♠️♣️') + }) + it('Should validate with minLength constraint', () => { + const T = Type.RegExp(/(.*)/, { + minLength: 3, + }) + Ok(T, 'xxx') + Fail(T, 'xx') + }) + it('Should validate with maxLength constraint', () => { + const T = Type.RegExp(/(.*)/, { + maxLength: 3, + }) + Ok(T, 'xxx') + Fail(T, 'xxxx') + }) +}) diff --git a/test/runtime/compiler/required.ts b/test/runtime/compiler/required.ts new file mode 100644 index 000000000..c94daf975 --- /dev/null +++ b/test/runtime/compiler/required.ts @@ -0,0 +1,55 @@ +import { Type, ReadonlyKind, OptionalKind } from '@sinclair/typebox' +import { Ok, Fail } from './validate' +import { strictEqual } from 'assert' + +describe('compiler/Required', () => { + 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()), + }, + { 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('Should update modifier types correctly when converting to required', () => { + const A = Type.Object({ + x: Type.Readonly(Type.Optional(Type.Number())), + y: Type.Readonly(Type.Number()), + z: Type.Optional(Type.Number()), + w: Type.Number(), + }) + const T = Type.Required(A) + strictEqual(T.properties.x[ReadonlyKind], 'Readonly') + strictEqual(T.properties.y[ReadonlyKind], 'Readonly') + strictEqual(T.properties.z[OptionalKind], undefined) + strictEqual(T.properties.w[OptionalKind], undefined) + }) + 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()), + }, + { additionalPropeties: false }, + ) + const T = Type.Required(A) + strictEqual(A.additionalPropeties, false) + strictEqual(T.additionalPropeties, false) + }) + + // it('Should construct new object when targetting reference', () => { + // const T = Type.Object({ a: Type.String(), b: Type.String() }, { $id: 'T' }) + // const R = Type.Ref(T) + // const P = Type.Required(R) + // strictEqual(P.properties.a.type, 'string') + // strictEqual(P.properties.b.type, 'string') + // }) +}) diff --git a/test/runtime/compiler/string-pattern.ts b/test/runtime/compiler/string-pattern.ts new file mode 100644 index 000000000..6ee16cbb0 --- /dev/null +++ b/test/runtime/compiler/string-pattern.ts @@ -0,0 +1,65 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler/StringPattern', () => { + //----------------------------------------------------- + // Regular Expression + //----------------------------------------------------- + it('Should validate regular expression 1', () => { + const T = Type.String({ pattern: /[012345]/.source }) + Ok(T, '0') + Ok(T, '1') + Ok(T, '2') + Ok(T, '3') + Ok(T, '4') + Ok(T, '5') + }) + it('Should validate regular expression 2', () => { + const T = Type.String({ pattern: /true|false/.source }) + Ok(T, 'true') + Ok(T, 'true') + Ok(T, 'true') + Ok(T, 'false') + Ok(T, 'false') + Ok(T, 'false') + Fail(T, '6') + }) + it('Should validate regular expression 3', () => { + const T = Type.String({ pattern: /true|false/.source }) + Fail(T, 'unknown') + }) + it('Should validate regular expression 4', () => { + const T = Type.String({ pattern: /[\d]{5}/.source }) + Ok(T, '12345') + }) + //----------------------------------------------------- + // Regular Pattern + //----------------------------------------------------- + it('Should validate pattern 1', () => { + const T = Type.String({ pattern: '[012345]' }) + Ok(T, '0') + Ok(T, '1') + Ok(T, '2') + Ok(T, '3') + Ok(T, '4') + Ok(T, '5') + }) + it('Should validate pattern 2', () => { + const T = Type.String({ pattern: '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 validate pattern 3', () => { + const T = Type.String({ pattern: 'true|false' }) + Fail(T, 'unknown') + }) + it('Should validate pattern 4', () => { + const T = Type.String({ pattern: '[\\d]{5}' }) + Ok(T, '12345') + }) +}) diff --git a/test/runtime/compiler/string.ts b/test/runtime/compiler/string.ts new file mode 100644 index 000000000..2086bc3bd --- /dev/null +++ b/test/runtime/compiler/string.ts @@ -0,0 +1,71 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler/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('Should not validate bigint', () => { + const T = Type.String() + Fail(T, BigInt(1)) + }) + it('Should not validate symbol', () => { + const T = Type.String() + Fail(T, Symbol(1)) + }) + it('Should validate string format as email', () => { + const T = Type.String({ format: 'email' }) + Ok(T, 'name@domain.com') + }) + it('Should validate string format as uuid', () => { + const T = Type.String({ format: 'uuid' }) + Ok(T, '4a7a17c9-2492-4a53-8e13-06ea2d3f3bbf') + }) + it('Should validate string format as iso8601 date', () => { + const T = Type.String({ format: 'date-time' }) + Ok(T, '2021-06-11T20:30:00-04:00') + }) + it('Should validate minLength', () => { + const T = Type.String({ minLength: 4 }) + Ok(T, '....') + Fail(T, '...') + }) + it('Should validate maxLength', () => { + const T = Type.String({ maxLength: 4 }) + Ok(T, '....') + Fail(T, '.....') + }) + it('Should pass numeric 5 digit test', () => { + const T = Type.String({ pattern: '[\\d]{5}' }) + Ok(T, '12345') + }) + it('Should should escape characters in the pattern', () => { + const T = Type.String({ pattern: '/a/' }) + Ok(T, '/a/') + }) +}) diff --git a/test/runtime/compiler/symbol.ts b/test/runtime/compiler/symbol.ts new file mode 100644 index 000000000..a35052612 --- /dev/null +++ b/test/runtime/compiler/symbol.ts @@ -0,0 +1,42 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler/Symbol', () => { + it('Should not validate a boolean', () => { + const T = Type.Symbol() + Fail(T, true) + Fail(T, false) + }) + it('Should not validate a number', () => { + const T = Type.Symbol() + Fail(T, 1) + }) + it('Should not validate a string', () => { + const T = Type.Symbol() + Fail(T, 'true') + }) + it('Should not validate an array', () => { + const T = Type.Symbol() + Fail(T, [true]) + }) + it('Should not validate an object', () => { + const T = Type.Symbol() + Fail(T, {}) + }) + it('Should not validate an null', () => { + const T = Type.Symbol() + Fail(T, null) + }) + it('Should not validate an undefined', () => { + const T = Type.Symbol() + Fail(T, undefined) + }) + it('Should not validate bigint', () => { + const T = Type.Symbol() + Fail(T, BigInt(1)) + }) + it('Should not validate symbol', () => { + const T = Type.Symbol() + Ok(T, Symbol(1)) + }) +}) diff --git a/test/runtime/compiler/template-literal.ts b/test/runtime/compiler/template-literal.ts new file mode 100644 index 000000000..7c0589cf0 --- /dev/null +++ b/test/runtime/compiler/template-literal.ts @@ -0,0 +1,209 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler/TemplateLiteral', () => { + // -------------------------------------------------------- + // Finite + // -------------------------------------------------------- + it('Should validate finite pattern 1', () => { + // prettier-ignore + const T = Type.TemplateLiteral([]) + Ok(T, '') + Fail(T, 'X') + }) + it('Should validate finite pattern 1', () => { + // prettier-ignore + const T = Type.TemplateLiteral([Type.Boolean()]) + Ok(T, 'true') + Ok(T, 'false') + Fail(T, 'X') + }) + it('Should validate finite pattern 2', () => { + // prettier-ignore + const T = Type.TemplateLiteral([ + Type.Literal('A') + ]) + Ok(T, 'A') + Fail(T, 'X') + }) + it('Should validate finite pattern 3', () => { + // prettier-ignore + const T = Type.TemplateLiteral([ + Type.Literal('A'), + Type.Literal('B') + ]) + Ok(T, 'AB') + Fail(T, 'X') + }) + it('Should validate finite pattern 4', () => { + // prettier-ignore + const T = Type.TemplateLiteral([ + Type.Literal('A'), + Type.Union([ + Type.Literal('B'), + Type.Literal('C') + ]), + ]) + Ok(T, 'AB') + Ok(T, 'AC') + Fail(T, 'X') + }) + it('Should validate finite pattern 5', () => { + // prettier-ignore + const T = Type.TemplateLiteral([ + Type.Literal('A'), + Type.Union([ + Type.Literal('B'), + Type.Literal('C') + ]), + Type.Literal('D'), + ]) + Ok(T, 'ABD') + Ok(T, 'ACD') + Fail(T, 'X') + }) + it('Should validate finite pattern 6', () => { + // prettier-ignore + const T = Type.TemplateLiteral([ + Type.Union([ + Type.Literal('0'), + Type.Literal('1') + ]), + Type.Union([ + Type.Literal('0'), + Type.Literal('1') + ]), + ]) + Ok(T, '00') + Ok(T, '01') + Ok(T, '10') + Ok(T, '11') + Fail(T, 'X') + }) + // -------------------------------------------------------- + // Infinite + // -------------------------------------------------------- + it('Should validate infinite pattern 1', () => { + // prettier-ignore + const T = Type.TemplateLiteral([ + Type.Number() + ]) + Ok(T, '1') + Ok(T, '22') + Ok(T, '333') + Ok(T, '4444') + Fail(T, 'X') + }) + it('Should validate infinite pattern 2', () => { + // prettier-ignore + const T = Type.TemplateLiteral([ + Type.Integer() + ]) + Ok(T, '1') + Ok(T, '22') + Ok(T, '333') + Ok(T, '4444') + Fail(T, 'X') + }) + it('Should validate infinite pattern 3', () => { + // prettier-ignore + const T = Type.TemplateLiteral([ + Type.BigInt() + ]) + Ok(T, '1') + Ok(T, '22') + Ok(T, '333') + Ok(T, '4444') + Fail(T, 'X') + }) + it('Should validate infinite pattern 4', () => { + // prettier-ignore + const T = Type.TemplateLiteral([ + Type.String() + ]) + Ok(T, '1') + Ok(T, '22') + Ok(T, '333') + Ok(T, '4444') + Ok(T, 'a') + Ok(T, 'bb') + Ok(T, 'ccc') + Ok(T, 'dddd') + }) + it('Should validate infinite pattern 5', () => { + // prettier-ignore + const T = Type.TemplateLiteral([ + Type.Literal('A'), + Type.Number() + ]) + Ok(T, 'A1') + Ok(T, 'A22') + Ok(T, 'A333') + Ok(T, 'A4444') + Fail(T, 'X') + }) + it('Should validate infinite pattern 6', () => { + // prettier-ignore + const T = Type.TemplateLiteral([ + Type.Literal('A'), + Type.Integer() + ]) + Ok(T, 'A1') + Ok(T, 'A22') + Ok(T, 'A333') + Ok(T, 'A4444') + Fail(T, 'X') + }) + it('Should validate infinite pattern 7', () => { + // prettier-ignore + const T = Type.TemplateLiteral([ + Type.Literal('A'), + Type.BigInt() + ]) + Ok(T, 'A1') + Ok(T, 'A22') + Ok(T, 'A333') + Ok(T, 'A4444') + Fail(T, 'X') + }) + it('Should validate infinite pattern 8', () => { + // prettier-ignore + const T = Type.TemplateLiteral([ + Type.Literal('A'), + Type.String() + ]) + Ok(T, 'A1') + Ok(T, 'A22') + Ok(T, 'A333') + Ok(T, 'A4444') + Ok(T, 'Aa') + Ok(T, 'Abb') + Ok(T, 'Accc') + Ok(T, 'Adddd') + Fail(T, 'X') + }) + it('Should validate enum (implicit)', () => { + enum E { + A, + B, + C, + } + const T = Type.TemplateLiteral([Type.Literal('hello'), Type.Enum(E)]) + Ok(T, 'hello0') + Ok(T, 'hello1') + Ok(T, 'hello2') + Fail(T, 'hello3') + }) + it('Should validate enum (explicit)', () => { + enum E { + A, + B = 'B', + C = 'C', + } + const T = Type.TemplateLiteral([Type.Literal('hello'), Type.Enum(E)]) + Ok(T, 'hello0') + Ok(T, 'helloB') + Ok(T, 'helloC') + Fail(T, 'helloD') + }) +}) diff --git a/test/runtime/compiler/tuple.ts b/test/runtime/compiler/tuple.ts new file mode 100644 index 000000000..9271681a1 --- /dev/null +++ b/test/runtime/compiler/tuple.ts @@ -0,0 +1,53 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler/Tuple', () => { + 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]) + }) + 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('Should validate with empty tuple', () => { + const T = Type.Tuple([]) + Ok(T, []) + }) + it('Should not validate with empty tuple with more items', () => { + const T = Type.Tuple([]) + Fail(T, [1]) + }) + it('Should not validate with empty tuple with less items', () => { + const T = Type.Tuple([Type.Number(), Type.Number()]) + Fail(T, [1]) + }) + 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 }]) + }) + 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/test/runtime/compiler/uint8array.ts b/test/runtime/compiler/uint8array.ts new file mode 100644 index 000000000..306498726 --- /dev/null +++ b/test/runtime/compiler/uint8array.ts @@ -0,0 +1,47 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler/Uint8Array', () => { + it('Should not validate number', () => { + const T = Type.Uint8Array() + Fail(T, 1) + }) + it('Should not validate string', () => { + const T = Type.Uint8Array() + Fail(T, 'hello') + }) + it('Should not validate boolean', () => { + const T = Type.Uint8Array() + Fail(T, true) + }) + it('Should not validate array', () => { + const T = Type.Uint8Array() + Fail(T, [1, 2, 3]) + }) + it('Should not validate object', () => { + const T = Type.Uint8Array() + Fail(T, { a: 1, b: 2 }) + }) + it('Should not validate null', () => { + const T = Type.Uint8Array() + Fail(T, null) + }) + it('Should not validate undefined', () => { + const T = Type.Uint8Array() + Fail(T, undefined) + }) + it('Should validate Uint8Array', () => { + const T = Type.Uint8Array() + Ok(T, new Uint8Array(100)) + }) + it('Should validate minByteLength', () => { + const T = Type.Uint8Array({ minByteLength: 4 }) + Ok(T, new Uint8Array(4)) + Fail(T, new Uint8Array(3)) + }) + it('Should validate maxByteLength', () => { + const T = Type.Uint8Array({ maxByteLength: 4 }) + Ok(T, new Uint8Array(4)) + Fail(T, new Uint8Array(5)) + }) +}) diff --git a/test/runtime/compiler/unicode.ts b/test/runtime/compiler/unicode.ts new file mode 100644 index 000000000..f5ef1718f --- /dev/null +++ b/test/runtime/compiler/unicode.ts @@ -0,0 +1,84 @@ +import { Type } from '@sinclair/typebox' +import { Ok } from './validate' + +describe('compiler/Unicode', () => { + // --------------------------------------------------------- + // Identifiers + // --------------------------------------------------------- + it('Should support unicode identifiers', () => { + const T = Type.Object( + { + x: Type.Number(), + y: Type.Number(), + }, + { + $id: '식별자', + }, + ) + Ok(T, { + x: 1, + y: 2, + }) + }) + it('Should support unicode identifier references', () => { + const R = Type.Object( + { + x: Type.Number(), + y: Type.Number(), + }, + { + $id: '식별자', + }, + ) + const T = Type.Object({ + vector: Type.Ref(R.$id!), + }) + Ok( + T, + { + vector: { + x: 1, + y: 2, + }, + }, + [R], + ) + }) + it('Should support unicode identifier recursion', () => { + const Node = Type.Recursive( + (Node) => + Type.Object({ + id: Type.String(), + nodes: Type.Array(Node), + }), + { + $id: '식별자', + }, + ) + Ok(Node, { + id: 'A', + nodes: [ + { + id: 'B', + nodes: [ + { + id: 'C', + nodes: [], + }, + ], + }, + ], + }) + }) + // --------------------------------------------------------- + // Properties + // --------------------------------------------------------- + it('Should support unicode properties', () => { + const T = Type.Object({ + 이름: Type.String(), + }) + Ok(T, { + 이름: 'dave', + }) + }) +}) diff --git a/test/runtime/compiler/union.ts b/test/runtime/compiler/union.ts new file mode 100644 index 000000000..7cd710678 --- /dev/null +++ b/test/runtime/compiler/union.ts @@ -0,0 +1,56 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler/Union', () => { + 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) + }) + 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('Should fail to validate for descriminated union types', () => { + const A = Type.Object({ kind: Type.Literal('A'), value: Type.String() }) + const B = Type.Object({ kind: Type.Literal('B'), value: Type.Number() }) + const T = Type.Union([A, B]) + Fail(T, { kind: 'A', value: 42 }) // expect { kind: 'A', value: string } + Fail(T, { kind: 'B', value: 'hello' }) // expect { kind: 'B', value: number } + }) + 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 + }) + 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('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 + }) + 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/test/runtime/compiler/unknown.ts b/test/runtime/compiler/unknown.ts new file mode 100644 index 000000000..124a9f4a6 --- /dev/null +++ b/test/runtime/compiler/unknown.ts @@ -0,0 +1,33 @@ +import { Type } from '@sinclair/typebox' +import { Ok } from './validate' + +describe('compiler/Unknown', () => { + 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/test/runtime/compiler/validate.ts b/test/runtime/compiler/validate.ts new file mode 100644 index 000000000..508d49738 --- /dev/null +++ b/test/runtime/compiler/validate.ts @@ -0,0 +1,119 @@ +import { TypeCompiler } from '@sinclair/typebox/compiler' +import { Value } from '@sinclair/typebox/value' +import { TSchema, FormatRegistry } from '@sinclair/typebox' + +// ------------------------------------------------------------------------- +// Test Formats: https://github.com/ajv-validator/ajv-formats/blob/master/src/formats.ts +// +// - date-time +// - email +// - uuid +// +// ------------------------------------------------------------------------- + +const EMAIL = /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i +const UUID = /^(?:urn:uuid:)?[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i +const DATE_TIME_SEPARATOR = /t|\s/i +const TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i +const DATE = /^(\d\d\d\d)-(\d\d)-(\d\d)$/ +const DAYS = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + +function isLeapYear(year: number): boolean { + return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) +} +function isDate(str: string): boolean { + const matches: string[] | null = DATE.exec(str) + if (!matches) return false + const year: number = +matches[1] + const month: number = +matches[2] + const day: number = +matches[3] + return month >= 1 && month <= 12 && day >= 1 && day <= (month === 2 && isLeapYear(year) ? 29 : DAYS[month]) +} +function isTime(str: string, strictTimeZone?: boolean): boolean { + const matches: string[] | null = TIME.exec(str) + if (!matches) return false + const hr: number = +matches[1] + const min: number = +matches[2] + const sec: number = +matches[3] + const tz: string | undefined = matches[4] + const tzSign: number = matches[5] === '-' ? -1 : 1 + const tzH: number = +(matches[6] || 0) + const tzM: number = +(matches[7] || 0) + if (tzH > 23 || tzM > 59 || (strictTimeZone && !tz)) return false + if (hr <= 23 && min <= 59 && sec < 60) return true + // leap second + const utcMin = min - tzM * tzSign + const utcHr = hr - tzH * tzSign - (utcMin < 0 ? 1 : 0) + return (utcHr === 23 || utcHr === -1) && (utcMin === 59 || utcMin === -1) && sec < 61 +} +function isDateTime(str: string, strictTimeZone?: boolean): boolean { + const dateTime: string[] = str.split(DATE_TIME_SEPARATOR) + return dateTime.length === 2 && isDate(dateTime[0]) && isTime(dateTime[1], strictTimeZone) +} +// ------------------------------------------------------------------------- +// Use Formats +// ------------------------------------------------------------------------- +FormatRegistry.Set('email', (value) => EMAIL.test(value)) +FormatRegistry.Set('uuid', (value) => UUID.test(value)) +FormatRegistry.Set('date-time', (value) => isDateTime(value, true)) +export function Ok(schema: T, data: unknown, references: any[] = []) { + const C = TypeCompiler.Compile(schema, references) + const result = C.Check(data) + + if (result !== Value.Check(schema, references, data)) { + throw Error('Compiler and Value Check disparity') + } + if (result === false) { + const errors = [...Value.Errors(schema, references, data)] + if (errors.length === 0) throw Error('expected at least 1 error') + } + if (result === true) { + const errors = [...Value.Errors(schema, references, data)] + if (errors.length > 0) throw Error('expected no errors') + } + if (!result) { + console.log('---------------------------') + console.log('type') + console.log('---------------------------') + console.log(JSON.stringify(schema, 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(result) + throw Error('expected ok') + } +} +export function Fail(schema: T, data: unknown, references: any[] = []) { + const C = TypeCompiler.Compile(schema, references) + const result = C.Check(data) + if (result !== Value.Check(schema, references, data)) { + throw Error('Compiler and Value Check disparity') + } + if (result === false) { + const errors = [...Value.Errors(schema, references, data)] + if (errors.length === 0) throw Error('expected at least 1 error') + } + if (result === true) { + const errors = [...Value.Errors(schema, references, data)] + if (errors.length > 0) throw Error('expected no errors') + } + if (result) { + console.log('---------------------------') + console.log('type') + console.log('---------------------------') + console.log(JSON.stringify(schema, 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('none') + throw Error('expected ok') + } +} diff --git a/test/runtime/compiler/void.ts b/test/runtime/compiler/void.ts new file mode 100644 index 000000000..4a203436c --- /dev/null +++ b/test/runtime/compiler/void.ts @@ -0,0 +1,37 @@ +import { Type } from '@sinclair/typebox' +import { Ok, Fail } from './validate' + +describe('compiler/Void', () => { + it('Should not validate number', () => { + const T = Type.Void() + Fail(T, 1) + }) + it('Should not validate string', () => { + const T = Type.Void() + Fail(T, 'hello') + }) + it('Should not validate boolean', () => { + const T = Type.Void() + Fail(T, true) + }) + it('Should not validate array', () => { + const T = Type.Void() + Fail(T, [1, 2, 3]) + }) + it('Should not validate object', () => { + const T = Type.Void() + Fail(T, { a: 1, b: 2 }) + }) + it('Should validate null', () => { + const T = Type.Void() + Fail(T, null) + }) + it('Should validate undefined', () => { + const T = Type.Void() + Ok(T, undefined) + }) + it('Should validate void 0', () => { + const T = Type.Void() + Ok(T, void 0) + }) +}) diff --git a/test/runtime/errors/index.ts b/test/runtime/errors/index.ts new file mode 100644 index 000000000..db746b1ff --- /dev/null +++ b/test/runtime/errors/index.ts @@ -0,0 +1,2 @@ +import './iterator/index' +import './types/index' diff --git a/test/runtime/errors/iterator/index.ts b/test/runtime/errors/iterator/index.ts new file mode 100644 index 000000000..41d3ac147 --- /dev/null +++ b/test/runtime/errors/iterator/index.ts @@ -0,0 +1 @@ +import './iterator' diff --git a/test/runtime/errors/iterator/iterator.ts b/test/runtime/errors/iterator/iterator.ts new file mode 100644 index 000000000..d32cd126d --- /dev/null +++ b/test/runtime/errors/iterator/iterator.ts @@ -0,0 +1,32 @@ +import { Type } from '@sinclair/typebox' +import { Errors } from '@sinclair/typebox/errors' +import { Assert } from '../../assert' + +describe('errors/ValueErrorIterator', () => { + it('Should return undefined for non error', () => { + const R = Errors(Type.Number(), [], 1).First() + Assert.IsEqual(R, undefined) + }) + it('Should return a value error when error', () => { + const { type, path, message } = Errors(Type.Number(), [], '').First()! + Assert.IsTypeOf(type, 'number') + Assert.IsTypeOf(path, 'string') + Assert.IsTypeOf(message, 'string') + }) + it('Should yield empty array for non error', () => { + const R = [...Errors(Type.Number(), [], 1)] + Assert.IsEqual(R.length, 0) + }) + it('Should yield array with 1 error when error', () => { + const R = [...Errors(Type.Number(), [], 'foo')] + Assert.IsEqual(R.length, 1) + }) + it('Should yield array with N errors when error', () => { + // prettier-ignore + const R = [...Errors(Type.Object({ + x: Type.Number(), + y: Type.Number() + }), [], {})] // require object to invoke internal check + Assert.IsEqual(R.length > 1, true) + }) +}) diff --git a/test/runtime/errors/types/array-contains.ts b/test/runtime/errors/types/array-contains.ts new file mode 100644 index 000000000..fe73e01f9 --- /dev/null +++ b/test/runtime/errors/types/array-contains.ts @@ -0,0 +1,17 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/ArrayMaxContainsItems', () => { + const T = Type.Array(Type.Any(), { contains: Type.Literal(1) }) + it('Should pass 0', () => { + const R = Resolve(T, [1]) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, [2]) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.ArrayContains) + }) +}) diff --git a/test/runtime/errors/types/array-max-contains.ts b/test/runtime/errors/types/array-max-contains.ts new file mode 100644 index 000000000..df55b1e10 --- /dev/null +++ b/test/runtime/errors/types/array-max-contains.ts @@ -0,0 +1,36 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/ArrayMaxContainsItems', () => { + const T = Type.Array(Type.Any(), { contains: Type.Literal(1), maxContains: 4 }) + it('Should pass 0', () => { + const R = Resolve(T, [1, 1, 1, 1]) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, null) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.Array) + }) + it('Should pass 2', () => { + const R = Resolve(T, [1, 1, 1, 1, 1]) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.ArrayMaxContains) + }) + it('Should pass 3', () => { + const R = Resolve(T, []) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.ArrayContains) + }) + it('Should pass 4', () => { + const R = Resolve(T, [1, 2, 3, 4]) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 5', () => { + const R = Resolve(T, [2, 3, 4]) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.ArrayContains) + }) +}) diff --git a/test/runtime/errors/types/array-max-items.ts b/test/runtime/errors/types/array-max-items.ts new file mode 100644 index 000000000..b24fdc86f --- /dev/null +++ b/test/runtime/errors/types/array-max-items.ts @@ -0,0 +1,22 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/ArrayMaxItems', () => { + const T = Type.Array(Type.Any(), { maxItems: 4 }) + it('Should pass 0', () => { + const R = Resolve(T, [1, 2, 3, 4]) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, null) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.Array) + }) + it('Should pass 2', () => { + const R = Resolve(T, [1, 2, 3, 4, 5]) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.ArrayMaxItems) + }) +}) diff --git a/test/runtime/errors/types/array-min-contains.ts b/test/runtime/errors/types/array-min-contains.ts new file mode 100644 index 000000000..a8240684a --- /dev/null +++ b/test/runtime/errors/types/array-min-contains.ts @@ -0,0 +1,28 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/ArrayMinContainsItems', () => { + const T = Type.Array(Type.Any(), { contains: Type.Literal(1), minContains: 4 }) + it('Should pass 0', () => { + const R = Resolve(T, [1, 1, 1, 1]) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, null) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.Array) + }) + it('Should pass 2', () => { + const R = Resolve(T, [1]) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.ArrayMinContains) + }) + it('Should pass 3', () => { + const R = Resolve(T, []) + Assert.IsEqual(R.length, 2) + Assert.IsEqual(R[0].type, ValueErrorType.ArrayContains) + Assert.IsEqual(R[1].type, ValueErrorType.ArrayMinContains) + }) +}) diff --git a/test/runtime/errors/types/array-min-items.ts b/test/runtime/errors/types/array-min-items.ts new file mode 100644 index 000000000..fc1770a08 --- /dev/null +++ b/test/runtime/errors/types/array-min-items.ts @@ -0,0 +1,22 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/ArrayMinItems', () => { + const T = Type.Array(Type.Any(), { minItems: 4 }) + it('Should pass 0', () => { + const R = Resolve(T, [1, 2, 3, 4]) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, null) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.Array) + }) + it('Should pass 2', () => { + const R = Resolve(T, []) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.ArrayMinItems) + }) +}) diff --git a/test/runtime/errors/types/array-unique-items.ts b/test/runtime/errors/types/array-unique-items.ts new file mode 100644 index 000000000..d6f502cd6 --- /dev/null +++ b/test/runtime/errors/types/array-unique-items.ts @@ -0,0 +1,21 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/ArrayUniqueItems', () => { + const T = Type.Array(Type.Any(), { uniqueItems: true }) + it('Should pass 0', () => { + const R = Resolve(T, [1, 2, 3, 4]) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 2', () => { + const R = Resolve(T, []) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 3', () => { + const R = Resolve(T, [1, 1, 3, 4]) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.ArrayUniqueItems) + }) +}) diff --git a/test/runtime/errors/types/array.ts b/test/runtime/errors/types/array.ts new file mode 100644 index 000000000..eb6904de2 --- /dev/null +++ b/test/runtime/errors/types/array.ts @@ -0,0 +1,27 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/Array', () => { + const T = Type.Array(Type.Any()) + it('Should pass 0', () => { + const R = Resolve(T, [1, 2, 3]) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, 1) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.Array) + }) + it('Should pass 2', () => { + const R = Resolve(T, {}) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.Array) + }) + it('Should pass 3', () => { + const R = Resolve(T, null) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.Array) + }) +}) diff --git a/test/runtime/errors/types/async-iterator.ts b/test/runtime/errors/types/async-iterator.ts new file mode 100644 index 000000000..55f6f17d8 --- /dev/null +++ b/test/runtime/errors/types/async-iterator.ts @@ -0,0 +1,17 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/AsyncIterator', () => { + const T = Type.AsyncIterator(Type.Any()) + it('Should pass 0', () => { + const R = Resolve(T, (async function* (): any {})()) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, 1) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.AsyncIterator) + }) +}) diff --git a/test/runtime/errors/types/bigint-exclusive-maximum.ts b/test/runtime/errors/types/bigint-exclusive-maximum.ts new file mode 100644 index 000000000..9acf831ef --- /dev/null +++ b/test/runtime/errors/types/bigint-exclusive-maximum.ts @@ -0,0 +1,17 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/BigIntExclusiveMaximum', () => { + const T = Type.BigInt({ exclusiveMaximum: 4n }) + it('Should pass 0', () => { + const R = Resolve(T, 0n) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, 4n) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.BigIntExclusiveMaximum) + }) +}) diff --git a/test/runtime/errors/types/bigint-exclusive-minimum.ts b/test/runtime/errors/types/bigint-exclusive-minimum.ts new file mode 100644 index 000000000..80a19f4d2 --- /dev/null +++ b/test/runtime/errors/types/bigint-exclusive-minimum.ts @@ -0,0 +1,17 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/BigIntExclusiveMinimum', () => { + const T = Type.BigInt({ exclusiveMinimum: 4n }) + it('Should pass 0', () => { + const R = Resolve(T, 5n) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, 4n) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.BigIntExclusiveMinimum) + }) +}) diff --git a/test/runtime/errors/types/bigint-maximum.ts b/test/runtime/errors/types/bigint-maximum.ts new file mode 100644 index 000000000..fe5f2090b --- /dev/null +++ b/test/runtime/errors/types/bigint-maximum.ts @@ -0,0 +1,17 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/BigIntMaximum', () => { + const T = Type.BigInt({ maximum: 4n }) + it('Should pass 0', () => { + const R = Resolve(T, 0n) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, 5n) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.BigIntMaximum) + }) +}) diff --git a/test/runtime/errors/types/bigint-minimum.ts b/test/runtime/errors/types/bigint-minimum.ts new file mode 100644 index 000000000..1f2177311 --- /dev/null +++ b/test/runtime/errors/types/bigint-minimum.ts @@ -0,0 +1,17 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/BigIntMinimum', () => { + const T = Type.BigInt({ minimum: 4n }) + it('Should pass 0', () => { + const R = Resolve(T, 4n) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, 3n) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.BigIntMinimum) + }) +}) diff --git a/test/runtime/errors/types/bigint-multiple-of.ts b/test/runtime/errors/types/bigint-multiple-of.ts new file mode 100644 index 000000000..f7fe277a1 --- /dev/null +++ b/test/runtime/errors/types/bigint-multiple-of.ts @@ -0,0 +1,17 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/BigIntMultipleOf', () => { + const T = Type.BigInt({ multipleOf: 2n }) + it('Should pass 0', () => { + const R = Resolve(T, 0n) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, 1n) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.BigIntMultipleOf) + }) +}) diff --git a/test/runtime/errors/types/bigint.ts b/test/runtime/errors/types/bigint.ts new file mode 100644 index 000000000..4f7ed05b3 --- /dev/null +++ b/test/runtime/errors/types/bigint.ts @@ -0,0 +1,17 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/BigInt', () => { + const T = Type.BigInt() + it('Should pass 0', () => { + const R = Resolve(T, 0n) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, 0) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.BigInt) + }) +}) diff --git a/test/runtime/errors/types/boolean.ts b/test/runtime/errors/types/boolean.ts new file mode 100644 index 000000000..5ed8edce1 --- /dev/null +++ b/test/runtime/errors/types/boolean.ts @@ -0,0 +1,17 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/Boolean', () => { + const T = Type.Boolean() + it('Should pass 0', () => { + const R = Resolve(T, true) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, 0) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.Boolean) + }) +}) diff --git a/test/runtime/errors/types/date-exclusive-maximum-timestamp.ts b/test/runtime/errors/types/date-exclusive-maximum-timestamp.ts new file mode 100644 index 000000000..ab1c29197 --- /dev/null +++ b/test/runtime/errors/types/date-exclusive-maximum-timestamp.ts @@ -0,0 +1,17 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/DateExclusiveMaximumTimestamp', () => { + const T = Type.Date({ exclusiveMaximumTimestamp: 4 }) + it('Should pass 0', () => { + const R = Resolve(T, new Date(0)) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, new Date(4)) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.DateExclusiveMaximumTimestamp) + }) +}) diff --git a/test/runtime/errors/types/date-exclusive-minimum-timestamp.ts b/test/runtime/errors/types/date-exclusive-minimum-timestamp.ts new file mode 100644 index 000000000..a9f439ef7 --- /dev/null +++ b/test/runtime/errors/types/date-exclusive-minimum-timestamp.ts @@ -0,0 +1,17 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/DateExclusiveMinimumTimestamp', () => { + const T = Type.Date({ exclusiveMinimumTimestamp: 4 }) + it('Should pass 0', () => { + const R = Resolve(T, new Date(5)) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, new Date(4)) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.DateExclusiveMinimumTimestamp) + }) +}) diff --git a/test/runtime/errors/types/date-maximum-timestamp.ts b/test/runtime/errors/types/date-maximum-timestamp.ts new file mode 100644 index 000000000..11e8805a3 --- /dev/null +++ b/test/runtime/errors/types/date-maximum-timestamp.ts @@ -0,0 +1,17 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/DateMaximumTimestamp', () => { + const T = Type.Date({ maximumTimestamp: 4 }) + it('Should pass 0', () => { + const R = Resolve(T, new Date(0)) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, new Date(5)) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.DateMaximumTimestamp) + }) +}) diff --git a/test/runtime/errors/types/date-minimum-timestamp.ts b/test/runtime/errors/types/date-minimum-timestamp.ts new file mode 100644 index 000000000..311fc474e --- /dev/null +++ b/test/runtime/errors/types/date-minimum-timestamp.ts @@ -0,0 +1,17 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/DateMinimumTimestamp', () => { + const T = Type.Date({ minimumTimestamp: 4 }) + it('Should pass 0', () => { + const R = Resolve(T, new Date(4)) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, new Date(3)) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.DateMinimumTimestamp) + }) +}) diff --git a/test/runtime/errors/types/date-multiple-of-timestamp.ts b/test/runtime/errors/types/date-multiple-of-timestamp.ts new file mode 100644 index 000000000..8c3404ac1 --- /dev/null +++ b/test/runtime/errors/types/date-multiple-of-timestamp.ts @@ -0,0 +1,17 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/DateMultipleOfTimestamp', () => { + const T = Type.Date({ multipleOfTimestamp: 2 }) + it('Should pass 0', () => { + const R = Resolve(T, new Date(0)) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, new Date(1)) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.DateMultipleOfTimestamp) + }) +}) diff --git a/test/runtime/errors/types/date.ts b/test/runtime/errors/types/date.ts new file mode 100644 index 000000000..01a553ec2 --- /dev/null +++ b/test/runtime/errors/types/date.ts @@ -0,0 +1,17 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/Date', () => { + const T = Type.Date() + it('Should pass 0', () => { + const R = Resolve(T, new Date()) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, 0) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.Date) + }) +}) diff --git a/test/runtime/errors/types/function.ts b/test/runtime/errors/types/function.ts new file mode 100644 index 000000000..4fbb628a8 --- /dev/null +++ b/test/runtime/errors/types/function.ts @@ -0,0 +1,17 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/Function', () => { + const T = Type.Function([], Type.Any()) + it('Should pass 0', () => { + const R = Resolve(T, function (): any {}) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, 1) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.Function) + }) +}) diff --git a/test/runtime/errors/types/index.ts b/test/runtime/errors/types/index.ts new file mode 100644 index 000000000..18084a348 --- /dev/null +++ b/test/runtime/errors/types/index.ts @@ -0,0 +1,68 @@ +import './array-contains' +import './array-max-contains' +import './array-min-items' +import './array-min-contains' +import './array-max-items' +import './array-unique-items' +import './array' +import './async-iterator' +import './bigint-exclusive-maximum' +import './bigint-exclusive-minimum' +import './bigint-maximum' +import './bigint-minimum' +import './bigint-multiple-of' +import './bigint' +import './boolean' +import './date-exclusive-maximum-timestamp' +import './date-exclusive-minimum-timestamp' +import './date-maximum-timestamp' +import './date-minimum-timestamp' +import './date-multiple-of-timestamp' +import './date' +import './function' +import './integer-exclusive-maximum' +import './integer-exclusive-minimum' +import './integer-maximum' +import './integer-minimum' +import './integer-multiple-of' +import './integer' +import './intersect-unevaluated-properties' +import './intersect' +import './iterator' +import './kind' +import './literal' +import './never' +import './not' +import './null' +import './number-exclusive-maximum' +import './number-exclusive-minimum' +import './number-maximum' +import './number-minimum' +import './number' +import './number-multiple-of' +import './object-additional-properties' +import './object-max-properties' +import './object-min-properties' +import './object-pointer-property' +import './object-required-property' +import './object' +import './promise' +import './record-pointer-property' +import './regexp-max-length' +import './regexp-min-length' +import './regexp' +import './string-format-unknown' +import './string-format' +import './string-max-length' +import './string-min-length' +import './string-pattern' +import './string' +import './symbol' +import './tuple-length' +import './tuple' +import './uint8array-max-byte-length' +import './uint8array-min-byte-length' +import './uint8array' +import './undefined' +import './union' +import './void' diff --git a/test/runtime/errors/types/integer-exclusive-maximum.ts b/test/runtime/errors/types/integer-exclusive-maximum.ts new file mode 100644 index 000000000..0667c1060 --- /dev/null +++ b/test/runtime/errors/types/integer-exclusive-maximum.ts @@ -0,0 +1,17 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/IntegerExclusiveMaximum', () => { + const T = Type.Integer({ exclusiveMaximum: 4 }) + it('Should pass 0', () => { + const R = Resolve(T, 0) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, 4) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.IntegerExclusiveMaximum) + }) +}) diff --git a/test/runtime/errors/types/integer-exclusive-minimum.ts b/test/runtime/errors/types/integer-exclusive-minimum.ts new file mode 100644 index 000000000..e59556a93 --- /dev/null +++ b/test/runtime/errors/types/integer-exclusive-minimum.ts @@ -0,0 +1,17 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/IntegerExclusiveMinimum', () => { + const T = Type.Integer({ exclusiveMinimum: 4 }) + it('Should pass 0', () => { + const R = Resolve(T, 5) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, 4) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.IntegerExclusiveMinimum) + }) +}) diff --git a/test/runtime/errors/types/integer-maximum.ts b/test/runtime/errors/types/integer-maximum.ts new file mode 100644 index 000000000..fbe028570 --- /dev/null +++ b/test/runtime/errors/types/integer-maximum.ts @@ -0,0 +1,17 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/IntegerMaximum', () => { + const T = Type.Integer({ maximum: 4 }) + it('Should pass 0', () => { + const R = Resolve(T, 0) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, 5) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.IntegerMaximum) + }) +}) diff --git a/test/runtime/errors/types/integer-minimum.ts b/test/runtime/errors/types/integer-minimum.ts new file mode 100644 index 000000000..f7590e110 --- /dev/null +++ b/test/runtime/errors/types/integer-minimum.ts @@ -0,0 +1,17 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/IntegerMinimum', () => { + const T = Type.Integer({ minimum: 4 }) + it('Should pass 0', () => { + const R = Resolve(T, 4) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, 3) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.IntegerMinimum) + }) +}) diff --git a/test/runtime/errors/types/integer-multiple-of.ts b/test/runtime/errors/types/integer-multiple-of.ts new file mode 100644 index 000000000..38a0e14ff --- /dev/null +++ b/test/runtime/errors/types/integer-multiple-of.ts @@ -0,0 +1,17 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/IntegerMultipleOf', () => { + const T = Type.Integer({ multipleOf: 2 }) + it('Should pass 0', () => { + const R = Resolve(T, 0) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, 1) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.IntegerMultipleOf) + }) +}) diff --git a/test/runtime/errors/types/integer.ts b/test/runtime/errors/types/integer.ts new file mode 100644 index 000000000..5f671ed63 --- /dev/null +++ b/test/runtime/errors/types/integer.ts @@ -0,0 +1,17 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/Integer', () => { + const T = Type.Integer() + it('Should pass 0', () => { + const R = Resolve(T, 0) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, 0n) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.Integer) + }) +}) diff --git a/test/runtime/errors/types/intersect-unevaluated-properties.ts b/test/runtime/errors/types/intersect-unevaluated-properties.ts new file mode 100644 index 000000000..58624b214 --- /dev/null +++ b/test/runtime/errors/types/intersect-unevaluated-properties.ts @@ -0,0 +1,36 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/IntersectUnevaluatedProperties', () => { + const T1 = Type.Intersect([Type.Object({ x: Type.Number() }), Type.Object({ y: Type.Number() })], { + unevaluatedProperties: false, + }) + it('Should pass 0', () => { + const R = Resolve(T1, { x: 1, y: 2 }) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T1, { x: 1, y: 2, z: 3 }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.IntersectUnevaluatedProperties) + }) + const T2 = Type.Intersect([Type.Object({ x: Type.Number() }), Type.Object({ y: Type.Number() })], { + unevaluatedProperties: Type.String(), + }) + it('Should pass 3', () => { + const R = Resolve(T2, { x: 1, y: 2, z: '1' }) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 4', () => { + const R = Resolve(T2, { x: 1, y: 2, a: 1, b: 2 }) + Assert.IsEqual(R.length, 2) + Assert.IsEqual(R[0].type, ValueErrorType.String) + Assert.IsEqual(R[0].path, '/a') + Assert.IsEqual(R[0].value, 1) + Assert.IsEqual(R[1].type, ValueErrorType.String) + Assert.IsEqual(R[1].path, '/b') + Assert.IsEqual(R[1].value, 2) + }) +}) diff --git a/test/runtime/errors/types/intersect.ts b/test/runtime/errors/types/intersect.ts new file mode 100644 index 000000000..5e1f8f163 --- /dev/null +++ b/test/runtime/errors/types/intersect.ts @@ -0,0 +1,49 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/Intersect', () => { + it('Should pass 0', () => { + const T = Type.Intersect([Type.Object({ x: Type.Number() }), Type.Object({ y: Type.Number() })]) + const R = Resolve(T, { x: 1, y: 1 }) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const T = Type.Intersect([Type.Object({ x: Type.Number() }), Type.Object({ y: Type.Number() })]) + const R = Resolve(T, { x: 1 }) + Assert.IsEqual(R.length, 3) + Assert.IsEqual(R[0].type, ValueErrorType.ObjectRequiredProperty) + Assert.IsEqual(R[1].type, ValueErrorType.Number) + Assert.IsEqual(R[2].type, ValueErrorType.Intersect) + }) + it('Should pass 2', () => { + const T = Type.Intersect([Type.Object({ x: Type.Number() }), Type.Object({ y: Type.Number() })]) + const R = Resolve(T, { y: 1 }) + Assert.IsEqual(R.length, 3) + Assert.IsEqual(R[0].type, ValueErrorType.ObjectRequiredProperty) + Assert.IsEqual(R[1].type, ValueErrorType.Number) + Assert.IsEqual(R[2].type, ValueErrorType.Intersect) + }) + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/887 + // ---------------------------------------------------------------- + it('Should pass 3', () => { + const A = Type.Intersect([Type.Object({ x: Type.Number() }), Type.Object({ y: Type.Number() })]) + const B = Type.Intersect([Type.Object({ x: Type.Number() }), Type.Object({ z: Type.Number() })]) + const T = Type.Intersect([A, B]) + const R = Resolve(T, {}) + Assert.IsEqual(R.length, 11) + Assert.IsEqual(R[0].type, ValueErrorType.ObjectRequiredProperty) + Assert.IsEqual(R[1].type, ValueErrorType.Number) + Assert.IsEqual(R[2].type, ValueErrorType.ObjectRequiredProperty) + Assert.IsEqual(R[3].type, ValueErrorType.Number) + Assert.IsEqual(R[4].type, ValueErrorType.Intersect) + Assert.IsEqual(R[5].type, ValueErrorType.ObjectRequiredProperty) + Assert.IsEqual(R[6].type, ValueErrorType.Number) + Assert.IsEqual(R[7].type, ValueErrorType.ObjectRequiredProperty) + Assert.IsEqual(R[8].type, ValueErrorType.Number) + Assert.IsEqual(R[9].type, ValueErrorType.Intersect) + Assert.IsEqual(R[10].type, ValueErrorType.Intersect) + }) +}) diff --git a/test/runtime/errors/types/iterator.ts b/test/runtime/errors/types/iterator.ts new file mode 100644 index 000000000..f1ceee268 --- /dev/null +++ b/test/runtime/errors/types/iterator.ts @@ -0,0 +1,17 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/Iterator', () => { + const T = Type.Iterator(Type.Any()) + it('Should pass 0', () => { + const R = Resolve(T, (function* (): any {})()) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, 1) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.Iterator) + }) +}) diff --git a/test/runtime/errors/types/kind.ts b/test/runtime/errors/types/kind.ts new file mode 100644 index 000000000..c8865353e --- /dev/null +++ b/test/runtime/errors/types/kind.ts @@ -0,0 +1,25 @@ +import { TypeRegistry, Kind, TSchema } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/Kind', () => { + // ---------------------------------------------------- + // Fixture + // ---------------------------------------------------- + beforeEach(() => TypeRegistry.Set('Foo', (schema, value) => value === 'foo')) + afterEach(() => TypeRegistry.Delete('Foo')) + // ---------------------------------------------------- + // Test + // ---------------------------------------------------- + const T = { [Kind]: 'Foo' } as TSchema + it('Should pass 0', () => { + const R = Resolve(T, 'foo') + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, 1) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.Kind) + }) +}) diff --git a/test/runtime/errors/types/literal.ts b/test/runtime/errors/types/literal.ts new file mode 100644 index 000000000..2fe351133 --- /dev/null +++ b/test/runtime/errors/types/literal.ts @@ -0,0 +1,46 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/Literal', () => { + // ----------------------------------------------------- + // LiteralString + // ----------------------------------------------------- + const T1 = Type.Literal('hello') + it('Should pass 0', () => { + const R = Resolve(T1, 'hello') + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T1, 'world') + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.Literal) + }) + // ----------------------------------------------------- + // LiteralNumber + // ----------------------------------------------------- + const T2 = Type.Literal(0) + it('Should pass 2', () => { + const R = Resolve(T2, 0) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 3', () => { + const R = Resolve(T2, 1) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.Literal) + }) + // ----------------------------------------------------- + // LiteralBoolean + // ----------------------------------------------------- + const T3 = Type.Literal(true) + it('Should pass 4', () => { + const R = Resolve(T3, true) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 5', () => { + const R = Resolve(T3, false) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.Literal) + }) +}) diff --git a/test/runtime/errors/types/never.ts b/test/runtime/errors/types/never.ts new file mode 100644 index 000000000..e85e808ff --- /dev/null +++ b/test/runtime/errors/types/never.ts @@ -0,0 +1,13 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/Never', () => { + const T = Type.Never() + it('Should pass 1', () => { + const R = Resolve(T, 0) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.Never) + }) +}) diff --git a/test/runtime/errors/types/not.ts b/test/runtime/errors/types/not.ts new file mode 100644 index 000000000..01d3605c5 --- /dev/null +++ b/test/runtime/errors/types/not.ts @@ -0,0 +1,17 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/Not', () => { + const T = Type.Not(Type.String()) + it('Should pass 0', () => { + const R = Resolve(T, true) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, 'hello') + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.Not) + }) +}) diff --git a/test/runtime/errors/types/null.ts b/test/runtime/errors/types/null.ts new file mode 100644 index 000000000..0b897dc6a --- /dev/null +++ b/test/runtime/errors/types/null.ts @@ -0,0 +1,17 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/Null', () => { + const T = Type.Null() + it('Should pass 0', () => { + const R = Resolve(T, null) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, 1) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.Null) + }) +}) diff --git a/test/runtime/errors/types/number-exclusive-maximum.ts b/test/runtime/errors/types/number-exclusive-maximum.ts new file mode 100644 index 000000000..b80ce9417 --- /dev/null +++ b/test/runtime/errors/types/number-exclusive-maximum.ts @@ -0,0 +1,17 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/NumberExclusiveMaximum', () => { + const T = Type.Number({ exclusiveMaximum: 4 }) + it('Should pass 0', () => { + const R = Resolve(T, 0) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, 4) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.NumberExclusiveMaximum) + }) +}) diff --git a/test/runtime/errors/types/number-exclusive-minimum.ts b/test/runtime/errors/types/number-exclusive-minimum.ts new file mode 100644 index 000000000..c474a8dd7 --- /dev/null +++ b/test/runtime/errors/types/number-exclusive-minimum.ts @@ -0,0 +1,17 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/NumberExclusiveMinimum', () => { + const T = Type.Number({ exclusiveMinimum: 4 }) + it('Should pass 0', () => { + const R = Resolve(T, 5) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, 4) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.NumberExclusiveMinimum) + }) +}) diff --git a/test/runtime/errors/types/number-maximum.ts b/test/runtime/errors/types/number-maximum.ts new file mode 100644 index 000000000..fa4a3ec1d --- /dev/null +++ b/test/runtime/errors/types/number-maximum.ts @@ -0,0 +1,17 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/NumberMaximum', () => { + const T = Type.Number({ maximum: 4 }) + it('Should pass 0', () => { + const R = Resolve(T, 0) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, 5) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.NumberMaximum) + }) +}) diff --git a/test/runtime/errors/types/number-minimum.ts b/test/runtime/errors/types/number-minimum.ts new file mode 100644 index 000000000..8d8f4bfc2 --- /dev/null +++ b/test/runtime/errors/types/number-minimum.ts @@ -0,0 +1,17 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/NumberMinimum', () => { + const T = Type.Number({ minimum: 4 }) + it('Should pass 0', () => { + const R = Resolve(T, 4) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, 3) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.NumberMinimum) + }) +}) diff --git a/test/runtime/errors/types/number-multiple-of.ts b/test/runtime/errors/types/number-multiple-of.ts new file mode 100644 index 000000000..921a2367d --- /dev/null +++ b/test/runtime/errors/types/number-multiple-of.ts @@ -0,0 +1,17 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/NumberMultipleOf', () => { + const T = Type.Number({ multipleOf: 2 }) + it('Should pass 0', () => { + const R = Resolve(T, 0) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, 1) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.NumberMultipleOf) + }) +}) diff --git a/test/runtime/errors/types/number.ts b/test/runtime/errors/types/number.ts new file mode 100644 index 000000000..0055c4dc1 --- /dev/null +++ b/test/runtime/errors/types/number.ts @@ -0,0 +1,17 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/Number', () => { + const T = Type.Number() + it('Should pass 0', () => { + const R = Resolve(T, 0) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, 0n) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.Number) + }) +}) diff --git a/test/runtime/errors/types/object-additional-properties.ts b/test/runtime/errors/types/object-additional-properties.ts new file mode 100644 index 000000000..9cad1bfc0 --- /dev/null +++ b/test/runtime/errors/types/object-additional-properties.ts @@ -0,0 +1,48 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/ObjectAdditionalProperties', () => { + // ---------------------------------------------------------- + // additionalProperties: false + // ---------------------------------------------------------- + const T1 = Type.Object( + { + x: Type.Number(), + }, + { additionalProperties: false }, + ) + it('Should pass 0', () => { + const R = Resolve(T1, { x: 1 }) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T1, { x: 1, y: 1 }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.ObjectAdditionalProperties) + }) + // ---------------------------------------------------------- + // additionalProperties: TSchema + // ---------------------------------------------------------- + const T2 = Type.Object( + { + x: Type.Number(), + }, + { additionalProperties: Type.String() }, + ) + it('Should pass 2', () => { + const R = Resolve(T2, { x: 1 }) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 3', () => { + const R = Resolve(T2, { x: 1, a: 1, b: 2 }) + Assert.IsEqual(R.length, 2) + Assert.IsEqual(R[0].type, ValueErrorType.String) + Assert.IsEqual(R[0].path, '/a') + Assert.IsEqual(R[0].value, 1) + Assert.IsEqual(R[1].type, ValueErrorType.String) + Assert.IsEqual(R[1].path, '/b') + Assert.IsEqual(R[1].value, 2) + }) +}) diff --git a/test/runtime/errors/types/object-max-properties.ts b/test/runtime/errors/types/object-max-properties.ts new file mode 100644 index 000000000..ee042e3f0 --- /dev/null +++ b/test/runtime/errors/types/object-max-properties.ts @@ -0,0 +1,17 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/ObjectMaxProperties', () => { + const T = Type.Object({}, { maxProperties: 2 }) + it('Should pass 0', () => { + const R = Resolve(T, { x: 1, y: 2 }) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, { x: 1, y: 2, z: 3 }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.ObjectMaxProperties) + }) +}) diff --git a/test/runtime/errors/types/object-min-properties.ts b/test/runtime/errors/types/object-min-properties.ts new file mode 100644 index 000000000..428a8b7e3 --- /dev/null +++ b/test/runtime/errors/types/object-min-properties.ts @@ -0,0 +1,17 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/ObjectMinProperties', () => { + const T = Type.Object({}, { minProperties: 2 }) + it('Should pass 0', () => { + const R = Resolve(T, { x: 1, y: 2 }) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, { x: 1 }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.ObjectMinProperties) + }) +}) diff --git a/test/runtime/errors/types/object-pointer-property.ts b/test/runtime/errors/types/object-pointer-property.ts new file mode 100644 index 000000000..ccf045896 --- /dev/null +++ b/test/runtime/errors/types/object-pointer-property.ts @@ -0,0 +1,139 @@ +import { Type } from '@sinclair/typebox' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/ObjectPointerProperty', () => { + // ---------------------------------------------------------------- + // Known + // ---------------------------------------------------------------- + it('Should produce known pointer property path 1', () => { + const T = Type.Object({ 'a/b': Type.String() }) + const R = Resolve(T, { 'a/b': 1 }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].path, '/a~1b') + }) + it('Should produce known pointer property path 2', () => { + const T = Type.Object({ 'a~b': Type.String() }) + const R = Resolve(T, { 'a~b': 1 }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].path, '/a~0b') + }) + it('Should produce known pointer property path 3', () => { + const T = Type.Object({ 'a/b~c': Type.String() }) + const R = Resolve(T, { 'a/b~c': 1 }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].path, '/a~1b~0c') + }) + it('Should produce known pointer property path 4', () => { + const T = Type.Object({ 'a~b/c': Type.String() }) + const R = Resolve(T, { 'a~b/c': 1 }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].path, '/a~0b~1c') + }) + it('Should produce known pointer property path 5', () => { + const T = Type.Object({ 'a~b/c/d': Type.String() }) + const R = Resolve(T, { 'a~b/c/d': 1 }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].path, '/a~0b~1c~1d') + }) + it('Should produce known pointer property path 6', () => { + const T = Type.Object({ 'a~b/c/d~e': Type.String() }) + const R = Resolve(T, { 'a~b/c/d~e': 1 }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].path, '/a~0b~1c~1d~0e') + }) + // ---------------------------------------------------------------- + // Unknown Additional + // ---------------------------------------------------------------- + it('Should produce unknown pointer property path 1', () => { + const T = Type.Object({}, { additionalProperties: false }) + const R = Resolve(T, { 'a/b': 1 }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].path, '/a~1b') + }) + it('Should produce unknown pointer property path 2', () => { + const T = Type.Object({}, { additionalProperties: false }) + const R = Resolve(T, { 'a~b': 1 }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].path, '/a~0b') + }) + // ---------------------------------------------------------------- + // Unknown Constrained + // ---------------------------------------------------------------- + it('Should produce unknown constrained pointer property path 1', () => { + const T = Type.Object({}, { additionalProperties: Type.String() }) + const R = Resolve(T, { 'a/b': 1 }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].path, '/a~1b') + }) + it('Should produce unknown constrained pointer property path 2', () => { + const T = Type.Object({}, { additionalProperties: Type.String() }) + const R = Resolve(T, { 'a~b': 1 }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].path, '/a~0b') + }) + // ---------------------------------------------------------------- + // Nested + // ---------------------------------------------------------------- + it('Should produce nested pointer 1', () => { + const T = Type.Object({ + 'x/y': Type.Object({ + z: Type.Object({ + w: Type.String(), + }), + }), + }) + const R = Resolve(T, { 'x/y': { z: { w: 1 } } }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].path, '/x~1y/z/w') + }) + it('Should produce nested pointer 2', () => { + const T = Type.Object({ + x: Type.Object({ + 'y/z': Type.Object({ + w: Type.String(), + }), + }), + }) + const R = Resolve(T, { x: { 'y/z': { w: 1 } } }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].path, '/x/y~1z/w') + }) + it('Should produce nested pointer 3', () => { + const T = Type.Object({ + x: Type.Object({ + y: Type.Object({ + 'z/w': Type.String(), + }), + }), + }) + const R = Resolve(T, { x: { y: { 'z/w': 1 } } }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].path, '/x/y/z~1w') + }) + // ---------------------------------------------------------------- + // Nested Array + // ---------------------------------------------------------------- + it('Should produce nested array pointer property path 1', () => { + const T = Type.Object({ + 'x/y': Type.Object({ + z: Type.Array(Type.String()), + }), + }) + const R = Resolve(T, { 'x/y': { z: ['a', 'b', 1] } }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].path, '/x~1y/z/2') + }) + it('Should produce nested array pointer property path 2', () => { + const T = Type.Object({ + x: Type.Array( + Type.Object({ + 'y/z': Type.String(), + }), + ), + }) + const R = Resolve(T, { x: [{ 'y/z': 'a' }, { 'y/z': 'b' }, { 'y/z': 1 }] }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].path, '/x/2/y~1z') + }) +}) diff --git a/test/runtime/errors/types/object-required-property.ts b/test/runtime/errors/types/object-required-property.ts new file mode 100644 index 000000000..4019885a7 --- /dev/null +++ b/test/runtime/errors/types/object-required-property.ts @@ -0,0 +1,22 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/ObjectRequiredProperty', () => { + const T = Type.Object({ x: Type.Number(), y: Type.Number() }) + it('Should pass 0', () => { + const R = Resolve(T, { x: 1, y: 2 }) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, { x: 1 }) + Assert.IsEqual(R.length, 2) + Assert.IsEqual(R[0].type, ValueErrorType.ObjectRequiredProperty) + Assert.IsEqual(R[0].path, '/y') + Assert.IsEqual(R[0].value, undefined) + Assert.IsEqual(R[1].type, ValueErrorType.Number) + Assert.IsEqual(R[1].path, '/y') + Assert.IsEqual(R[1].value, undefined) + }) +}) diff --git a/test/runtime/errors/types/object.ts b/test/runtime/errors/types/object.ts new file mode 100644 index 000000000..3e9b86de8 --- /dev/null +++ b/test/runtime/errors/types/object.ts @@ -0,0 +1,65 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/Object', () => { + // ----------------------------------------------------------------- + // Object + // ----------------------------------------------------------------- + const T1 = Type.Object({ x: Type.Number(), y: Type.Number() }) + it('Should pass 0', () => { + const R = Resolve(T1, { x: 1, y: 2 }) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T1, null) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.Object) + }) + // ----------------------------------------------------------------- + // Object: Optional + // ----------------------------------------------------------------- + const T2 = Type.Object({ x: Type.Optional(Type.Number()) }) + it('Should pass 2', () => { + const R = Resolve(T2, {}) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 3', () => { + const R = Resolve(T2, { x: 1 }) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 4', () => { + const R = Resolve(T2, { x: '' }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.Number) + Assert.IsEqual(R[0].path, '/x') + Assert.IsEqual(R[0].value, '') + }) + // ----------------------------------------------------------------- + // Object: Optional Multiple + // ----------------------------------------------------------------- + const T3 = Type.Partial(Type.Object({ x: Type.Number(), y: Type.Number() })) + it('Should pass 5', () => { + const R = Resolve(T3, {}) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 6', () => { + const R = Resolve(T3, { x: 1 }) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 7', () => { + const R = Resolve(T3, { y: 1 }) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 8', () => { + const R = Resolve(T3, { x: 'a', y: 'b' }) + Assert.IsEqual(R.length, 2) + Assert.IsEqual(R[0].type, ValueErrorType.Number) + Assert.IsEqual(R[0].path, '/x') + Assert.IsEqual(R[0].value, 'a') + Assert.IsEqual(R[1].type, ValueErrorType.Number) + Assert.IsEqual(R[1].path, '/y') + Assert.IsEqual(R[1].value, 'b') + }) +}) diff --git a/test/runtime/errors/types/promise.ts b/test/runtime/errors/types/promise.ts new file mode 100644 index 000000000..81dbe426a --- /dev/null +++ b/test/runtime/errors/types/promise.ts @@ -0,0 +1,17 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/Promise', () => { + const T = Type.Promise(Type.Any()) + it('Should pass 0', () => { + const R = Resolve(T, Promise.resolve(0)) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, 0) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.Promise) + }) +}) diff --git a/test/runtime/errors/types/record-pointer-property.ts b/test/runtime/errors/types/record-pointer-property.ts new file mode 100644 index 000000000..a2dbd3436 --- /dev/null +++ b/test/runtime/errors/types/record-pointer-property.ts @@ -0,0 +1,70 @@ +import { Type } from '@sinclair/typebox' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/RecordPointerProperty', () => { + // ---------------------------------------------------------------- + // Known + // ---------------------------------------------------------------- + it('Should produce known pointer property path 1', () => { + const T = Type.Record(Type.String(), Type.String()) + const R = Resolve(T, { 'a/b': 1 }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].path, '/a~1b') + }) + it('Should produce known pointer property path 2', () => { + const T = Type.Record(Type.String(), Type.String()) + const R = Resolve(T, { 'a~b': 1 }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].path, '/a~0b') + }) + // ---------------------------------------------------------------- + // Unknown + // ---------------------------------------------------------------- + it('Should produce unknown pointer property path 1', () => { + const T = Type.Record(Type.Number(), Type.String(), { + additionalProperties: false, + }) + const R = Resolve(T, { 'a/b': 1 }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].path, '/a~1b') + }) + it('Should produce unknown pointer property path 1', () => { + const T = Type.Record(Type.Number(), Type.String(), { + additionalProperties: false, + }) + const R = Resolve(T, { 'a~b': 1 }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].path, '/a~0b') + }) + // ---------------------------------------------------------------- + // Unknown Constrained + // ---------------------------------------------------------------- + it('Should produce unknown constrained pointer property path 1', () => { + const T = Type.Record(Type.Number(), Type.String(), { + additionalProperties: Type.String(), + }) + const R = Resolve(T, { 'a/b': 1 }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].path, '/a~1b') + }) + it('Should produce unknown constrained pointer property path 1', () => { + const T = Type.Record(Type.Number(), Type.String(), { + additionalProperties: Type.String(), + }) + const R = Resolve(T, { 'a~b': 1 }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].path, '/a~0b') + }) + // ---------------------------------------------------------------- + // PatternProperties + // ---------------------------------------------------------------- + it('Should produce pattern pointer property path 1', () => { + const T = Type.Record(Type.TemplateLiteral('${string}/${string}/c'), Type.String(), { + additionalProperties: false, + }) + const R = Resolve(T, { 'x/y/z': 1 }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].path, '/x~1y~1z') + }) +}) diff --git a/test/runtime/errors/types/regexp-max-length.ts b/test/runtime/errors/types/regexp-max-length.ts new file mode 100644 index 000000000..22f8fd7c9 --- /dev/null +++ b/test/runtime/errors/types/regexp-max-length.ts @@ -0,0 +1,19 @@ +import { Type, FormatRegistry } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/RegExpMaxLength', () => { + const T = Type.RegExp(/(.*)/, { + maxLength: 4, + }) + it('Should pass 0', () => { + const R = Resolve(T, 'xxxx') + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, 'xxxxx') + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.StringMaxLength) + }) +}) diff --git a/test/runtime/errors/types/regexp-min-length.ts b/test/runtime/errors/types/regexp-min-length.ts new file mode 100644 index 000000000..3bd6227bb --- /dev/null +++ b/test/runtime/errors/types/regexp-min-length.ts @@ -0,0 +1,19 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/RegExpMinLength', () => { + const T = Type.RegExp(/(.*)/, { + minLength: 4, + }) + it('Should pass 0', () => { + const R = Resolve(T, 'xxxx') + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, 'xxx') + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.StringMinLength) + }) +}) diff --git a/test/runtime/errors/types/regexp.ts b/test/runtime/errors/types/regexp.ts new file mode 100644 index 000000000..e6f1c69b8 --- /dev/null +++ b/test/runtime/errors/types/regexp.ts @@ -0,0 +1,17 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/RegExp', () => { + const T = Type.RegExp(/123/) + it('Should pass 0', () => { + const R = Resolve(T, '123') + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, '321') + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.RegExp) + }) +}) diff --git a/test/runtime/errors/types/resolve.ts b/test/runtime/errors/types/resolve.ts new file mode 100644 index 000000000..e636137be --- /dev/null +++ b/test/runtime/errors/types/resolve.ts @@ -0,0 +1,7 @@ +import { Errors } from '@sinclair/typebox/errors' +import { TSchema } from '@sinclair/typebox' + +/** Resolves errors */ +export function Resolve(schema: T, value: unknown) { + return [...Errors(schema, value)] +} diff --git a/test/runtime/errors/types/string-format-unknown.ts b/test/runtime/errors/types/string-format-unknown.ts new file mode 100644 index 000000000..b40e9622d --- /dev/null +++ b/test/runtime/errors/types/string-format-unknown.ts @@ -0,0 +1,13 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/StringFormatUnknown', () => { + const T = Type.String({ format: 'unknown' }) + it('Should pass 1', () => { + const R = Resolve(T, '') + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.StringFormatUnknown) + }) +}) diff --git a/test/runtime/errors/types/string-format.ts b/test/runtime/errors/types/string-format.ts new file mode 100644 index 000000000..d4f03453c --- /dev/null +++ b/test/runtime/errors/types/string-format.ts @@ -0,0 +1,25 @@ +import { Type, FormatRegistry } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/StringFormat', () => { + // ---------------------------------------------- + // Fixture + // ---------------------------------------------- + beforeEach(() => FormatRegistry.Set('foo', (value) => value === 'foo')) + afterEach(() => FormatRegistry.Delete('foo')) + // ---------------------------------------------- + // Tests + // ---------------------------------------------- + const T = Type.String({ format: 'foo' }) + it('Should pass 0', () => { + const R = Resolve(T, 'foo') + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, 'bar') + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.StringFormat) + }) +}) diff --git a/test/runtime/errors/types/string-max-length.ts b/test/runtime/errors/types/string-max-length.ts new file mode 100644 index 000000000..973ab2665 --- /dev/null +++ b/test/runtime/errors/types/string-max-length.ts @@ -0,0 +1,17 @@ +import { Type, FormatRegistry } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/StringMaxLength', () => { + const T = Type.String({ maxLength: 4 }) + it('Should pass 0', () => { + const R = Resolve(T, '1234') + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, '12345') + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.StringMaxLength) + }) +}) diff --git a/test/runtime/errors/types/string-min-length.ts b/test/runtime/errors/types/string-min-length.ts new file mode 100644 index 000000000..aa96f9257 --- /dev/null +++ b/test/runtime/errors/types/string-min-length.ts @@ -0,0 +1,17 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/StringMinLength', () => { + const T = Type.String({ minLength: 4 }) + it('Should pass 0', () => { + const R = Resolve(T, '1234') + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, '123') + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.StringMinLength) + }) +}) diff --git a/test/runtime/errors/types/string-pattern.ts b/test/runtime/errors/types/string-pattern.ts new file mode 100644 index 000000000..07c2f0655 --- /dev/null +++ b/test/runtime/errors/types/string-pattern.ts @@ -0,0 +1,17 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/StringPattern', () => { + const T = Type.String({ pattern: '123' }) + it('Should pass 0', () => { + const R = Resolve(T, '123') + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, '321') + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.StringPattern) + }) +}) diff --git a/test/runtime/errors/types/string.ts b/test/runtime/errors/types/string.ts new file mode 100644 index 000000000..79003ccfd --- /dev/null +++ b/test/runtime/errors/types/string.ts @@ -0,0 +1,17 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/String', () => { + const T = Type.String() + it('Should pass 0', () => { + const R = Resolve(T, 'hello') + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, 1) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.String) + }) +}) diff --git a/test/runtime/errors/types/symbol.ts b/test/runtime/errors/types/symbol.ts new file mode 100644 index 000000000..ced5c216b --- /dev/null +++ b/test/runtime/errors/types/symbol.ts @@ -0,0 +1,17 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/Symbol', () => { + const T = Type.Symbol() + it('Should pass 0', () => { + const R = Resolve(T, Symbol(1)) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, 1) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.Symbol) + }) +}) diff --git a/test/runtime/errors/types/tuple-length.ts b/test/runtime/errors/types/tuple-length.ts new file mode 100644 index 000000000..e8265e63d --- /dev/null +++ b/test/runtime/errors/types/tuple-length.ts @@ -0,0 +1,56 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/TupleLength', () => { + // ---------------------------------------------- + // Tuple: Empty + // ---------------------------------------------- + const T1 = Type.Tuple([]) + it('Should pass 0', () => { + const R = Resolve(T1, []) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T1, [1]) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.TupleLength) + }) + // ---------------------------------------------- + // Tuple: One + // ---------------------------------------------- + const T2 = Type.Tuple([Type.Number()]) + it('Should pass 2', () => { + const R = Resolve(T2, [1]) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 3', () => { + const R = Resolve(T2, []) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.TupleLength) + }) + it('Should pass 4', () => { + const R = Resolve(T2, [1, 1]) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.TupleLength) + }) + // ---------------------------------------------- + // Tuple: Element + // ---------------------------------------------- + const T3 = Type.Tuple([Type.Number(), Type.Number()]) + it('Should pass 5', () => { + const R = Resolve(T3, [1, 1]) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 6', () => { + const R = Resolve(T3, ['a', 'b']) + Assert.IsEqual(R.length, 2) + Assert.IsEqual(R[0].type, ValueErrorType.Number) + Assert.IsEqual(R[0].path, '/0') + Assert.IsEqual(R[0].value, 'a') + Assert.IsEqual(R[1].type, ValueErrorType.Number) + Assert.IsEqual(R[1].path, '/1') + Assert.IsEqual(R[1].value, 'b') + }) +}) diff --git a/test/runtime/errors/types/tuple.ts b/test/runtime/errors/types/tuple.ts new file mode 100644 index 000000000..acfa15070 --- /dev/null +++ b/test/runtime/errors/types/tuple.ts @@ -0,0 +1,17 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/Tuple', () => { + const T = Type.Tuple([]) + it('Should pass 0', () => { + const R = Resolve(T, []) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, 1) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.Tuple) + }) +}) diff --git a/test/runtime/errors/types/uint8array-max-byte-length.ts b/test/runtime/errors/types/uint8array-max-byte-length.ts new file mode 100644 index 000000000..4e530e1aa --- /dev/null +++ b/test/runtime/errors/types/uint8array-max-byte-length.ts @@ -0,0 +1,17 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/Uint8ArrayMaxByteLength', () => { + const T = Type.Uint8Array({ maxByteLength: 4 }) + it('Should pass 0', () => { + const R = Resolve(T, new Uint8Array(4)) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, new Uint8Array(5)) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.Uint8ArrayMaxByteLength) + }) +}) diff --git a/test/runtime/errors/types/uint8array-min-byte-length.ts b/test/runtime/errors/types/uint8array-min-byte-length.ts new file mode 100644 index 000000000..0ecc7c1b7 --- /dev/null +++ b/test/runtime/errors/types/uint8array-min-byte-length.ts @@ -0,0 +1,17 @@ +import { Type, FormatRegistry } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/Uint8ArrayMinByteLength', () => { + const T = Type.Uint8Array({ minByteLength: 4 }) + it('Should pass 0', () => { + const R = Resolve(T, new Uint8Array(4)) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, new Uint8Array(3)) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.Uint8ArrayMinByteLength) + }) +}) diff --git a/test/runtime/errors/types/uint8array.ts b/test/runtime/errors/types/uint8array.ts new file mode 100644 index 000000000..51b99dbc5 --- /dev/null +++ b/test/runtime/errors/types/uint8array.ts @@ -0,0 +1,17 @@ +import { Type, FormatRegistry } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/Uint8Array', () => { + const T = Type.Uint8Array() + it('Should pass 0', () => { + const R = Resolve(T, new Uint8Array(4)) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, null) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.Uint8Array) + }) +}) diff --git a/test/runtime/errors/types/undefined.ts b/test/runtime/errors/types/undefined.ts new file mode 100644 index 000000000..ac3be5d4f --- /dev/null +++ b/test/runtime/errors/types/undefined.ts @@ -0,0 +1,17 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/Undefined', () => { + const T = Type.Undefined() + it('Should pass 0', () => { + const R = Resolve(T, undefined) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, 0) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.Undefined) + }) +}) diff --git a/test/runtime/errors/types/union.ts b/test/runtime/errors/types/union.ts new file mode 100644 index 000000000..70d1389a3 --- /dev/null +++ b/test/runtime/errors/types/union.ts @@ -0,0 +1,23 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorIterator, ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/Union', () => { + const T = Type.Union([Type.String(), Type.Number()]) + it('Should pass 0', () => { + const R = Resolve(T, '1') + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, 1) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 2', () => { + const R = Resolve(T, true) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.Union) + Assert.IsEqual(R[0].errors[0].First()?.type, ValueErrorType.String) + Assert.IsEqual(R[0].errors[1].First()?.type, ValueErrorType.Number) + }) +}) diff --git a/test/runtime/errors/types/void.ts b/test/runtime/errors/types/void.ts new file mode 100644 index 000000000..cd5132f78 --- /dev/null +++ b/test/runtime/errors/types/void.ts @@ -0,0 +1,32 @@ +import { Type } from '@sinclair/typebox' +import { ValueErrorType } from '@sinclair/typebox/errors' +import { Resolve } from './resolve' +import { Assert } from '../../assert' +import { TypeSystemPolicy } from '@sinclair/typebox/system' + +describe('errors/type/Void', () => { + const T = Type.Void() + it('Should pass 0', () => { + const R = Resolve(T, void 0) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 1', () => { + const R = Resolve(T, undefined) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 2', () => { + const R = Resolve(T, void 1) + Assert.IsEqual(R.length, 0) + }) + it('Should pass 3', () => { + const R = Resolve(T, null) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].type, ValueErrorType.Void) + }) + it('Should pass 4', () => { + TypeSystemPolicy.AllowNullVoid = true + const R = Resolve(T, null) + Assert.IsEqual(R.length, 0) + TypeSystemPolicy.AllowNullVoid = false + }) +}) diff --git a/test/runtime/index.ts b/test/runtime/index.ts new file mode 100644 index 000000000..ec19724e0 --- /dev/null +++ b/test/runtime/index.ts @@ -0,0 +1,14 @@ +import { TypeSystemPolicy } from '@sinclair/typebox/system' + +// ------------------------------------------------------------------ +// InstanceMode: Freeze (Detect Unintended Side Effects) +// ------------------------------------------------------------------ +TypeSystemPolicy.InstanceMode = 'freeze' + +import './compiler/index' +import './compiler-ajv/index' +import './errors/index' +import './syntax/index' +import './system/index' +import './type/index' +import './value/index' diff --git a/test/runtime/syntax/index.ts b/test/runtime/syntax/index.ts new file mode 100644 index 000000000..8d81ae4fb --- /dev/null +++ b/test/runtime/syntax/index.ts @@ -0,0 +1 @@ +import './syntax' diff --git a/test/runtime/syntax/syntax.ts b/test/runtime/syntax/syntax.ts new file mode 100644 index 000000000..148119a7d --- /dev/null +++ b/test/runtime/syntax/syntax.ts @@ -0,0 +1,422 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Syntax } from '@sinclair/typebox/syntax' +import { Assert } from '../assert/index' + +// prettier-ignore +describe('syntax/Syntax', () => { + // ---------------------------------------------------------------- + // Type Expressions + // ---------------------------------------------------------------- + it('Should parse Any', () => { + const T = Syntax(`any`) + Assert.IsTrue(TypeGuard.IsAny(T)) + }) + it('Should parse Array 1', () => { + const T = Syntax(`number[]`) + Assert.IsTrue(TypeGuard.IsArray(T)) + Assert.IsTrue(TypeGuard.IsNumber(T.items)) + }) + it('Should parse Array 2', () => { + const T = Syntax(`Array`) + Assert.IsTrue(TypeGuard.IsArray(T)) + Assert.IsTrue(TypeGuard.IsNumber(T.items)) + }) + it('Should parse AsyncIterator', () => { + const T = Syntax(`AsyncIterator`) + Assert.IsTrue(TypeGuard.IsAsyncIterator(T)) + Assert.IsTrue(TypeGuard.IsNumber(T.items)) + }) + it('Should parse Awaited', () => { + const T = Syntax(`Awaited>`) + Assert.IsTrue(TypeGuard.IsNumber(T)) + }) + it('Should parse BigInt', () => { + const T = Syntax(`bigint`) + Assert.IsTrue(TypeGuard.IsBigInt(T)) + }) + it('Should parse Boolean', () => { + const T = Syntax(`boolean`) + Assert.IsTrue(TypeGuard.IsBoolean(T)) + }) + it('Should parse ConstructorParameters', () => { + const T = Syntax(`ConstructorParameters boolean>`) + Assert.IsTrue(TypeGuard.IsTuple(T)) + Assert.IsTrue(TypeGuard.IsNumber(T.items![0])) + Assert.IsTrue(TypeGuard.IsString(T.items![1])) + }) + it('Should parse Constructor', () => { + const T = Syntax(`new (a: number, b: string) => boolean`) + Assert.IsTrue(TypeGuard.IsConstructor(T)) + Assert.IsTrue(TypeGuard.IsNumber(T.parameters[0])) + Assert.IsTrue(TypeGuard.IsString(T.parameters[1])) + Assert.IsTrue(TypeGuard.IsBoolean(T.returns)) + }) + it('Should parse Date', () => { + const T = Syntax(`Date`) + Assert.IsTrue(TypeGuard.IsDate(T)) + }) + it('Should parse Exclude', () => { + const T = Syntax(`Exclude<1 | 2 | 3, 1>`) + Assert.IsTrue(TypeGuard.IsUnion(T)) + Assert.IsTrue(T.anyOf[0].const === 2) + Assert.IsTrue(T.anyOf[1].const === 3) + }) + it('Should parse Extract', () => { + const T = Syntax(`Extract<1 | 2 | 3, 1 | 2>`) + Assert.IsTrue(TypeGuard.IsUnion(T)) + // @ts-ignore fix: incorrect union order (result of UnionToTuple, replace with Tuple destructuring) + Assert.IsTrue(T.anyOf[0].const === 1) + // @ts-ignore fix: incorrect union order (result of UnionToTuple, replace with Tuple destructuring) + Assert.IsTrue(T.anyOf[1].const === 2) + }) + it('Should parse Function', () => { + const T = Syntax(`(a: number, b: string) => boolean`) + Assert.IsTrue(TypeGuard.IsFunction(T)) + Assert.IsTrue(TypeGuard.IsNumber(T.parameters[0])) + Assert.IsTrue(TypeGuard.IsString(T.parameters[1])) + Assert.IsTrue(TypeGuard.IsBoolean(T.returns)) + }) + it('Should parse Indexed 1', () => { + const T = Syntax(`{ x: 1, y: 2, z: 3 }['x']`) + Assert.IsTrue(T.const === 1) + }) + it('Should parse Indexed 2', () => { + const T = Syntax(`{ x: 1, y: 2, z: 3 }['x' | 'y' | 'z']`) + Assert.IsTrue(TypeGuard.IsUnion(T)) + Assert.IsTrue(T.anyOf[0].const === 1) + Assert.IsTrue(T.anyOf[1].const === 2) + Assert.IsTrue(T.anyOf[2].const === 3) + }) + it('Should parse Indexed 3', () => { + const T = Syntax(`{ x: 1, y: 2, z: 3 }`) + const S = Syntax({ T }, `T[keyof T]`) + Assert.IsTrue(TypeGuard.IsUnion(S)) + Assert.IsTrue(S.anyOf[0].const === 1) + Assert.IsTrue(S.anyOf[1].const === 2) + Assert.IsTrue(S.anyOf[2].const === 3) + }) + it('Should parse Indexed 4', () => { + const T = Syntax(`['A', 'B', 'C']`) + const S = Syntax({ T }, `T[number]`) + Assert.IsTrue(TypeGuard.IsUnion(S)) + Assert.IsTrue(S.anyOf[0].const === 'A') + Assert.IsTrue(S.anyOf[1].const === 'B') + Assert.IsTrue(S.anyOf[2].const === 'C') + }) + it('Should parse Integer', () => { + const T = Syntax(`integer`) + Assert.IsTrue(TypeGuard.IsInteger(T)) + }) + it('Should parse Intersect 1', () => { + const T = Syntax(`1 & 2`) + Assert.IsTrue(TypeGuard.IsIntersect(T)) + Assert.IsTrue(T.allOf[0].const === 1) + Assert.IsTrue(T.allOf[1].const === 2) + }) + it('Should parse Intersect 2', () => { + const T = Syntax(`1 & (2 & 3)`) // expect flatten + Assert.IsTrue(TypeGuard.IsIntersect(T)) + Assert.IsTrue(T.allOf[0].const === 1) + Assert.IsTrue(T.allOf[1].const === 2) + Assert.IsTrue(T.allOf[2].const === 3) + }) + it('Should parse Intersect 3', () => { + const T = Syntax(`(1 | 2) & 3`) // operator precedence + Assert.IsTrue(TypeGuard.IsIntersect(T)) + Assert.IsTrue(TypeGuard.IsUnion(T.allOf[0])) + Assert.IsTrue(T.allOf[1].const === 3) + }) + it('Should parse InstanceType 1', () => { + const T = Syntax(`InstanceType { x: 1, y: 2 }>`) + Assert.IsTrue(TypeGuard.IsObject(T)) + Assert.IsTrue(T.properties.x.const === 1) + Assert.IsTrue(T.properties.y.const === 2) + }) + it('Should parse InstanceType 2', () => { + const T = Syntax(`InstanceType`) // generalization issue + Assert.IsTrue(TypeGuard.IsNever(T)) + }) + it('Should parse Iterator', () => { + const T = Syntax(`Iterator`) + Assert.IsTrue(TypeGuard.IsIterator(T)) + Assert.IsTrue(TypeGuard.IsNumber(T.items)) + }) + it('Should parse KeyOf 1', () => { + const T = Syntax(`keyof { x: 1, y: 2 }`) + Assert.IsTrue(TypeGuard.IsUnion(T)) + Assert.IsTrue(T.anyOf[0].const === 'x') + Assert.IsTrue(T.anyOf[1].const === 'y') + }) + it('Should parse KeyOf 2', () => { + const T = Syntax(`keyof [0, 1]`) + Assert.IsTrue(TypeGuard.IsUnion(T)) + Assert.IsTrue(T.anyOf[0].const === '0') + Assert.IsTrue(T.anyOf[1].const === '1') + }) + it('Should parse KeyOf 3', () => { + const T = Syntax(`{ x: 1, y: 2 }`) + const S = Syntax({ T }, `keyof T`) + Assert.IsTrue(TypeGuard.IsUnion(S)) + Assert.IsTrue(S.anyOf[0].const === 'x') + Assert.IsTrue(S.anyOf[1].const === 'y') + }) + it('Should parse Literal Boolean 1', () => { + const T = Syntax(`true`) + Assert.IsTrue(T.const === true) + }) + it('Should parse Literal Boolean 2', () => { + const T = Syntax(`false`) + Assert.IsTrue(T.const === false) + }) + it('Should parse Literal Number', () => { + const T = Syntax(`1`) + Assert.IsTrue(T.const === 1) + }) + it('Should parse Literal String', () => { + const T = Syntax(`'1'`) + Assert.IsTrue(T.const === '1') + }) + it('Should parse Mapped (Pending)', () => { + const T = Syntax(`{ [K in 1 | 2 | 3]: K }`) + Assert.IsTrue(T.const === 'Mapped types not supported') + }) + it('Should parse Never', () => { + const T = Syntax(`never`) + Assert.IsTrue(TypeGuard.IsNever(T)) + }) + it('Should parse Null', () => { + const T = Syntax(`null`) + Assert.IsTrue(TypeGuard.IsNull(T)) + }) + it('Should parse Number', () => { + const T = Syntax(`number`) + Assert.IsTrue(TypeGuard.IsNumber(T)) + }) + it('Should parse Object 1', () => { + const T = Syntax(`{x: boolean, y: number, z: string, }`) + Assert.IsTrue(TypeGuard.IsObject(T)) + Assert.IsTrue(TypeGuard.IsBoolean(T.properties.x)) + Assert.IsTrue(TypeGuard.IsNumber(T.properties.y)) + Assert.IsTrue(TypeGuard.IsString(T.properties.z)) + }) + it('Should parse Object 2', () => { + const T = Syntax(`{x: boolean; y: number; z: string; }`) + Assert.IsTrue(TypeGuard.IsObject(T)) + Assert.IsTrue(TypeGuard.IsBoolean(T.properties.x)) + Assert.IsTrue(TypeGuard.IsNumber(T.properties.y)) + Assert.IsTrue(TypeGuard.IsString(T.properties.z)) + }) + it('Should parse Object 3', () => { + const T = Syntax(`{ + x: boolean + y: number + z: string + }`) + Assert.IsTrue(TypeGuard.IsObject(T)) + Assert.IsTrue(TypeGuard.IsBoolean(T.properties.x)) + Assert.IsTrue(TypeGuard.IsNumber(T.properties.y)) + Assert.IsTrue(TypeGuard.IsString(T.properties.z)) + }) + it('Should parse Object 4', () => { + const T = Syntax(`{ + x: boolean; + y: number; + z: string; + }`) + Assert.IsTrue(TypeGuard.IsObject(T)) + Assert.IsTrue(TypeGuard.IsBoolean(T.properties.x)) + Assert.IsTrue(TypeGuard.IsNumber(T.properties.y)) + Assert.IsTrue(TypeGuard.IsString(T.properties.z)) + }) + it('Should parse Object 5', () => { + const T = Syntax(`{ + x: boolean, + y: number, + z: string, + }`) + Assert.IsTrue(TypeGuard.IsObject(T)) + Assert.IsTrue(TypeGuard.IsBoolean(T.properties.x)) + Assert.IsTrue(TypeGuard.IsNumber(T.properties.y)) + Assert.IsTrue(TypeGuard.IsString(T.properties.z)) + }) + it('Should parse Omit 1', () => { + const T = Syntax(`Omit<{ x: boolean, y: number, z: string }, 'z'>`) + Assert.IsTrue(TypeGuard.IsObject(T)) + Assert.IsTrue(TypeGuard.IsBoolean(T.properties.x)) + Assert.IsTrue(TypeGuard.IsNumber(T.properties.y)) + Assert.IsTrue(('z' in T.properties) === false) + }) + it('Should parse Omit 2', () => { + const T = Syntax(`Omit<{ x: boolean, y: number, z: string }, 'z' | 'y'>`) + Assert.IsTrue(TypeGuard.IsObject(T)) + Assert.IsTrue(TypeGuard.IsBoolean(T.properties.x)) + Assert.IsTrue(('y' in T.properties) === false) + Assert.IsTrue(('z' in T.properties) === false) + }) + it('Should parse Parameters', () => { + const T = Syntax(`Parameters<(a: number, b: string) => boolean>`) + Assert.IsTrue(TypeGuard.IsTuple(T)) + Assert.IsTrue(TypeGuard.IsNumber(T.items![0])) + Assert.IsTrue(TypeGuard.IsString(T.items![1])) + }) + it('Should parse Partial', () => { + const T = Syntax(`Partial<{ x: boolean, y: number, z: string }>`) + Assert.IsTrue(TypeGuard.IsObject(T)) + Assert.IsTrue(('required' in T) === false) + }) + it('Should parse Pick 1', () => { + const T = Syntax(`Pick<{ x: boolean, y: number, z: string }, 'x' | 'y'>`) + Assert.IsTrue(TypeGuard.IsObject(T)) + Assert.IsTrue(TypeGuard.IsBoolean(T.properties.x)) + Assert.IsTrue(TypeGuard.IsNumber(T.properties.y)) + Assert.IsTrue(('z' in T.properties) === false) + }) + it('Should parse Pick 2', () => { + const T = Syntax(`Pick<{ x: boolean, y: number, z: string }, 'x'>`) + Assert.IsTrue(TypeGuard.IsObject(T)) + Assert.IsTrue(TypeGuard.IsBoolean(T.properties.x)) + Assert.IsTrue(('y' in T.properties) === false) + Assert.IsTrue(('z' in T.properties) === false) + }) + it('Should parse Promise', () => { + const T = Syntax(`Promise`) + Assert.IsTrue(TypeGuard.IsPromise(T)) + Assert.IsTrue(TypeGuard.IsNumber(T.item)) + }) + it('Should parse ReadonlyOptional', () => { + const T = Syntax(`{ readonly x?: boolean, readonly y?: number }`) + Assert.IsTrue(TypeGuard.IsObject(T)) + Assert.IsTrue(TypeGuard.IsBoolean(T.properties.x)) + Assert.IsTrue(TypeGuard.IsReadonly(T.properties.x)) + Assert.IsTrue(TypeGuard.IsOptional(T.properties.x)) + Assert.IsTrue(TypeGuard.IsNumber(T.properties.y)) + Assert.IsTrue(TypeGuard.IsReadonly(T.properties.y)) + Assert.IsTrue(TypeGuard.IsOptional(T.properties.y)) + }) + it('Should parse Readonly', () => { + const T = Syntax(`{ readonly x: boolean, readonly y: number }`) + Assert.IsTrue(TypeGuard.IsObject(T)) + Assert.IsTrue(TypeGuard.IsBoolean(T.properties.x)) + Assert.IsTrue(TypeGuard.IsReadonly(T.properties.x)) + Assert.IsTrue(TypeGuard.IsNumber(T.properties.y)) + Assert.IsTrue(TypeGuard.IsReadonly(T.properties.y)) + }) + it('Should parse Record 1', () => { + const T = Syntax(`Record`) + Assert.IsTrue(TypeGuard.IsRecord(T)) + Assert.IsTrue(TypeGuard.IsNumber(T.patternProperties['^(.*)$'])) + }) + it('Should parse Record 2', () => { + const T = Syntax(`Record`) + Assert.IsTrue(TypeGuard.IsRecord(T)) + Assert.IsTrue(TypeGuard.IsNumber(T.patternProperties['^(0|[1-9][0-9]*)$'])) + }) + it('Should parse Record 3', () => { + const T = Syntax(`Record<'x' | 'y', number>`) + Assert.IsTrue(TypeGuard.IsObject(T)) + Assert.IsTrue(TypeGuard.IsNumber(T.properties.x)) + Assert.IsTrue(TypeGuard.IsNumber(T.properties.y)) + }) + it('Should parse Recursive', () => { + const T = Type.Recursive(This => Syntax({ This }, `{ id: string, nodes: This[] }`)) + Assert.IsTrue(TypeGuard.IsObject(T)) + Assert.IsTrue(TypeGuard.IsString(T.properties.id)) + Assert.IsTrue(TypeGuard.IsArray(T.properties.nodes)) + Assert.IsTrue(TypeGuard.IsThis(T.properties.nodes.items)) + }) + it('Should parse Ref', () => { + const T = Syntax('foo') + Assert.IsTrue(TypeGuard.IsRef(T)) + Assert.IsTrue(T.$ref === 'foo') + }) + it('Should parse Required', () => { + const T = Syntax(`Required<{ x?: boolean, y?: number, z?: string }>`) + Assert.IsTrue(TypeGuard.IsObject(T)) + Assert.IsEqual(T.required, ['x', 'y', 'z']) + }) + it('Should parse ReturnType 1', () => { + const T = Syntax(`ReturnType<() => { x: 1, y: 2 }>`) + Assert.IsTrue(TypeGuard.IsObject(T)) + Assert.IsTrue(T.properties.x.const === 1) + Assert.IsTrue(T.properties.y.const === 2) + }) + it('Should parse ReturnType 2', () => { + const T = Syntax(`ReturnType`) // generalization issue + Assert.IsTrue(TypeGuard.IsNever(T)) + }) + it('Should parse String', () => { + const T = Syntax(`string`) + Assert.IsTrue(TypeGuard.IsString(T)) + }) + it('Should parse Symbol', () => { + const T = Syntax(`symbol`) + Assert.IsTrue(TypeGuard.IsSymbol(T)) + }) + it('Should parse Tuple', () => { + const T = Syntax(`[0, 1, 2, 3]`) + Assert.IsTrue(TypeGuard.IsTuple(T)) + Assert.IsTrue(T.items![0].const === 0) + Assert.IsTrue(T.items![1].const === 1) + Assert.IsTrue(T.items![2].const === 2) + Assert.IsTrue(T.items![3].const === 3) + }) + it('Should parse Uint8Array', () => { + const T = Syntax(`Uint8Array`) + Assert.IsTrue(TypeGuard.IsUint8Array(T)) + }) + it('Should parse Undefined', () => { + const T = Syntax(`undefined`) + Assert.IsTrue(TypeGuard.IsUndefined(T)) + }) + it('Should parse Union 1', () => { + const T = Syntax(`1 | 2`) + Assert.IsTrue(TypeGuard.IsUnion(T)) + Assert.IsTrue(T.anyOf[0].const === 1) + Assert.IsTrue(T.anyOf[1].const === 2) + }) + it('Should parse Union 2', () => { + const T = Syntax(`1 | (2 | 3)`) + Assert.IsTrue(TypeGuard.IsUnion(T)) + Assert.IsTrue(T.anyOf[0].const === 1) + Assert.IsTrue(T.anyOf[1].const === 2) + Assert.IsTrue(T.anyOf[2].const === 3) + }) + it('Should parse Unknown', () => { + const T = Syntax(`unknown`) + Assert.IsTrue(TypeGuard.IsUnknown(T)) + }) + it('Should parse Void', () => { + const T = Syntax(`void`) + Assert.IsTrue(TypeGuard.IsVoid(T)) + }) + // ---------------------------------------------------------------- + // Argument + Instantiation + // ---------------------------------------------------------------- + it('Should parse Argument 0', () => { + const G = Syntax(`[Argument<0>]`) + const T = Syntax({ G }, `G`) + Assert.IsTrue(TypeGuard.IsTuple(T)) + Assert.IsTrue(TypeGuard.IsArgument(T.items![0])) + }) + it('Should parse Argument 1', () => { + const G = Syntax(`[Argument<0>]`) + const T = Syntax({ G }, `G`) + Assert.IsTrue(TypeGuard.IsTuple(T)) + Assert.IsTrue(TypeGuard.IsNumber(T.items![0])) + }) + it('Should parse Argument 2', () => { + const G = Syntax(`[Argument<0>, Argument<1>]`) + const T = Syntax({ G }, `G`) + Assert.IsTrue(TypeGuard.IsTuple(T)) + Assert.IsTrue(TypeGuard.IsNumber(T.items![0])) + Assert.IsTrue(TypeGuard.IsString(T.items![1])) + }) + it('Should parse Argument 3', () => { + const G = Syntax(`[Argument<0>, Argument<1>]`) + const T = Syntax({ G }, `G`) + Assert.IsTrue(TypeGuard.IsTuple(T)) + Assert.IsTrue(TypeGuard.IsNumber(T.items![0])) + Assert.IsTrue(TypeGuard.IsUnknown(T.items![1])) + }) +}) diff --git a/test/runtime/system/index.ts b/test/runtime/system/index.ts new file mode 100644 index 000000000..8365111b5 --- /dev/null +++ b/test/runtime/system/index.ts @@ -0,0 +1,2 @@ +import './policy/index' +import './type/index' diff --git a/test/runtime/system/policy/allow-array-object.ts b/test/runtime/system/policy/allow-array-object.ts new file mode 100644 index 000000000..7e23d5491 --- /dev/null +++ b/test/runtime/system/policy/allow-array-object.ts @@ -0,0 +1,65 @@ +import { Ok, Fail } from '../../compiler/validate' +import { TypeSystemPolicy } from '@sinclair/typebox/system' +import { Type } from '@sinclair/typebox' + +describe('system/TypeSystemPolicy/AllowArrayObject', () => { + beforeEach(() => { + TypeSystemPolicy.AllowArrayObject = true + }) + afterEach(() => { + TypeSystemPolicy.AllowArrayObject = false + }) + // --------------------------------------------------------------- + // Object + // --------------------------------------------------------------- + it('Should validate arrays with empty objects', () => { + const T = Type.Object({}) + Ok(T, [0, 1, 2]) + }) + it('Should validate arrays with objects with length property', () => { + const T = Type.Object({ length: Type.Number() }) + Ok(T, [0, 1, 2]) + }) + it('Should validate arrays with objects with additionalProperties false when array has no elements', () => { + const T = Type.Object({ length: Type.Number() }, { additionalProperties: false }) + Ok(T, []) + }) + it('Should not validate arrays with objects with additionalProperties false when array has elements', () => { + const T = Type.Object({ length: Type.Number() }, { additionalProperties: false }) + Fail(T, [0, 1, 2]) + }) + it('Should not validate arrays with objects when length property is string', () => { + const T = Type.Object({ length: Type.String() }) + Fail(T, [0, 1, 2]) + }) + // --------------------------------------------------------------- + // Record + // --------------------------------------------------------------- + it('Should validate arrays as Records with String Keys', () => { + const T = Type.Record(Type.String(), Type.Number()) + Ok(T, [0, 1, 2]) + }) + it('Should validate arrays as Records with Number Keys', () => { + const T = Type.Record(Type.Number(), Type.Number()) + Ok(T, [0, 1, 2]) + }) + it('Should validate arrays as Records with Integer Keys', () => { + const T = Type.Record(Type.Integer(), Type.Number()) + Ok(T, [0, 1, 2]) + }) + it('Should not validate arrays as Records with Object Values', () => { + const T = Type.Record( + Type.String(), + Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }), + ) + Ok(T, [ + { x: 1, y: 1, z: 1 }, + { x: 1, y: 1, z: 1 }, + { x: 1, y: 1, z: 1 }, + ]) + }) +}) diff --git a/test/runtime/system/policy/allow-nan.ts b/test/runtime/system/policy/allow-nan.ts new file mode 100644 index 000000000..54ec151cf --- /dev/null +++ b/test/runtime/system/policy/allow-nan.ts @@ -0,0 +1,62 @@ +import { Ok, Fail } from '../../compiler/validate' +import { TypeSystemPolicy } from '@sinclair/typebox/system' +import { Type } from '@sinclair/typebox' + +describe('system/TypeSystemPolicy/AllowNaN', () => { + beforeEach(() => { + TypeSystemPolicy.AllowNaN = true + }) + afterEach(() => { + TypeSystemPolicy.AllowNaN = false + }) + // --------------------------------------------------------------- + // Number + // --------------------------------------------------------------- + it('Should validate number with NaN', () => { + const T = Type.Number() + Ok(T, NaN) + }) + it('Should validate number with +Infinity', () => { + const T = Type.Number() + Ok(T, Infinity) + }) + it('Should validate number with -Infinity', () => { + const T = Type.Number() + Ok(T, -Infinity) + }) + // --------------------------------------------------------------- + // Integer + // + // Note: The Number.isInteger() test will fail for NaN. Because + // of this we cannot reasonably override NaN handling for integers. + // --------------------------------------------------------------- + it('Should not validate integer with NaN', () => { + const T = Type.Integer() + Fail(T, NaN) + }) + it('Should not validate integer with +Infinity', () => { + const T = Type.Integer() + Fail(T, Infinity) + }) + it('Should not validate integer with -Infinity', () => { + const T = Type.Integer() + Fail(T, -Infinity) + }) + // --------------------------------------------------------------- + // BigInt + // + // Note: We expect failures here as bigint isn't IEEE754 + // --------------------------------------------------------------- + it('Should not validate bigint with NaN', () => { + const T = Type.BigInt() + Fail(T, NaN) + }) + it('Should not validate bigint with +Infinity', () => { + const T = Type.BigInt() + Fail(T, Infinity) + }) + it('Should not validate bigint with -Infinity', () => { + const T = Type.BigInt() + Fail(T, -Infinity) + }) +}) diff --git a/test/runtime/system/policy/allow-null-void.ts b/test/runtime/system/policy/allow-null-void.ts new file mode 100644 index 000000000..5903890fa --- /dev/null +++ b/test/runtime/system/policy/allow-null-void.ts @@ -0,0 +1,31 @@ +import { Ok } from '../../compiler/validate' +import { TypeSystemPolicy } from '@sinclair/typebox/system' +import { Type } from '@sinclair/typebox' + +describe('system/TypeSystemPolicy/AllowNullVoid', () => { + beforeEach(() => { + TypeSystemPolicy.AllowNullVoid = true + }) + afterEach(() => { + TypeSystemPolicy.AllowNullVoid = false + }) + // --------------------------------------------------------------- + // Object + // --------------------------------------------------------------- + it('Should validate with null', () => { + const T = Type.Void() + Ok(T, null) + }) + it('Should validate with undefined', () => { + const T = Type.Void() + Ok(T, undefined) + }) + it('Should validate with void 0', () => { + const T = Type.Void() + Ok(T, void 0) + }) + it('Should validate with void 1', () => { + const T = Type.Void() + Ok(T, void 1) + }) +}) diff --git a/test/runtime/system/policy/exact-optional-property-types.ts b/test/runtime/system/policy/exact-optional-property-types.ts new file mode 100644 index 000000000..6c6e63bca --- /dev/null +++ b/test/runtime/system/policy/exact-optional-property-types.ts @@ -0,0 +1,39 @@ +import { Ok, Fail } from '../../compiler/validate' +import { TypeSystemPolicy } from '@sinclair/typebox/system' +import { Type } from '@sinclair/typebox' + +describe('system/TypeSystemPolicy/ExactOptionalPropertyTypes', () => { + beforeEach(() => { + TypeSystemPolicy.ExactOptionalPropertyTypes = true + }) + afterEach(() => { + TypeSystemPolicy.ExactOptionalPropertyTypes = false + }) + // --------------------------------------------------------------- + // Number + // --------------------------------------------------------------- + it('Should not validate optional number', () => { + const T = Type.Object({ + x: Type.Optional(Type.Number()), + }) + Ok(T, {}) + Ok(T, { x: 1 }) + Fail(T, { x: undefined }) + }) + it('Should not validate undefined', () => { + const T = Type.Object({ + x: Type.Optional(Type.Undefined()), + }) + Ok(T, {}) + Fail(T, { x: 1 }) + Ok(T, { x: undefined }) + }) + it('Should validate optional number | undefined', () => { + const T = Type.Object({ + x: Type.Optional(Type.Union([Type.Number(), Type.Undefined()])), + }) + Ok(T, {}) + Ok(T, { x: 1 }) + Ok(T, { x: undefined }) + }) +}) diff --git a/test/runtime/system/policy/index.ts b/test/runtime/system/policy/index.ts new file mode 100644 index 000000000..144b64afe --- /dev/null +++ b/test/runtime/system/policy/index.ts @@ -0,0 +1,5 @@ +import './allow-array-object' +import './allow-nan' +import './allow-null-void' +import './exact-optional-property-types' +import './instance-mode' diff --git a/test/runtime/system/policy/instance-mode.ts b/test/runtime/system/policy/instance-mode.ts new file mode 100644 index 000000000..1e400c247 --- /dev/null +++ b/test/runtime/system/policy/instance-mode.ts @@ -0,0 +1,34 @@ +import { TypeSystemPolicy } from '@sinclair/typebox/system' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('system/TypeSystemPolicy/InstanceMode', () => { + after(() => { + TypeSystemPolicy.InstanceMode = 'freeze' + }) + // --------------------------------------------------------------- + // Number + // --------------------------------------------------------------- + it('Should use instance mode default', () => { + TypeSystemPolicy.InstanceMode = 'default' + const S = Type.String() + const T = Type.Array(S) + S.$id = 'updated' + Assert.IsEqual(T.items.$id, 'updated') + }) + it('Should use instance mode clone', () => { + TypeSystemPolicy.InstanceMode = 'clone' + const S = Type.String() + const T = Type.Array(S) + S.$id = 'updated' + Assert.IsEqual(T.items.$id, undefined) + }) + it('Should use instance mode freeze', () => { + TypeSystemPolicy.InstanceMode = 'freeze' + Assert.Throws(() => { + const S = Type.String() + const T = Type.Array(S) + S.$id = 'updated' + }) + }) +}) diff --git a/test/runtime/system/type/format.ts b/test/runtime/system/type/format.ts new file mode 100644 index 000000000..bac7c9160 --- /dev/null +++ b/test/runtime/system/type/format.ts @@ -0,0 +1,19 @@ +import { Ok, Fail } from '../../compiler/validate' +import { Assert } from '../../assert/index' +import { TypeSystem } from '@sinclair/typebox/system' +import { Type, FormatRegistry } from '@sinclair/typebox' + +describe('system/TypeSystem/Format', () => { + it('Should create and validate a format', () => { + const Foo = TypeSystem.Format('Foo', (value) => value === 'foo') + const T = Type.String({ format: Foo }) + Ok(T, 'foo') + Fail(T, 'bar') + FormatRegistry.Delete('Foo') + }) + it('Should throw if registering the same type twice', () => { + TypeSystem.Format('Foo', () => true) + Assert.Throws(() => TypeSystem.Format('Foo', () => true)) + FormatRegistry.Delete('Foo') + }) +}) diff --git a/test/runtime/system/type/index.ts b/test/runtime/system/type/index.ts new file mode 100644 index 000000000..1afccae94 --- /dev/null +++ b/test/runtime/system/type/index.ts @@ -0,0 +1,2 @@ +import './format' +import './type' diff --git a/test/runtime/system/type/type.ts b/test/runtime/system/type/type.ts new file mode 100644 index 000000000..86288f595 --- /dev/null +++ b/test/runtime/system/type/type.ts @@ -0,0 +1,22 @@ +import { Ok, Fail } from '../../compiler/validate' +import { TypeSystem } from '@sinclair/typebox/system' +import { TypeRegistry } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('system/TypeSystem/Type', () => { + it('Should create and validate a type', () => { + const Foo = TypeSystem.Type('Foo', (options, value) => { + Assert.IsEqual(options.option, 'test') + return value === 'foo' + }) + const T = Foo({ option: 'test' }) + Ok(T, 'foo') + Fail(T, 'bar') + TypeRegistry.Delete('Foo') + }) + it('Should throw if registering the same type twice', () => { + TypeSystem.Type('Foo', () => true) + Assert.Throws(() => TypeSystem.Type('Foo', () => true)) + TypeRegistry.Delete('Foo') + }) +}) diff --git a/test/runtime/type/clone/clone.ts b/test/runtime/type/clone/clone.ts new file mode 100644 index 000000000..36568003b --- /dev/null +++ b/test/runtime/type/clone/clone.ts @@ -0,0 +1,147 @@ +// -------------------------------------------------------------------- +// $id deletion was omitted from 0.26.0 to reduce complexity overhead. +// -------------------------------------------------------------------- + +// import { Type } from '@sinclair/typebox' +// import { Assert } from '../../assert' + +// describe('type/Clone', () => { +// it('Should retain source type $id for cloned objects', () => { +// const S = Type.String({ $id: 'S' }) +// const T = Type.Object({ x: S, y: S }) +// Assert.equal(T.properties.x.$id, undefined) +// Assert.equal(T.properties.y.$id, undefined) +// Assert.equal(S.$id, 'S') +// }) +// it('Should retain source type $id when composing objects with cloned arrays', () => { +// const S = Type.String({ $id: 'S' }) +// const T = Type.Function([S], S) +// Assert.equal(S.$id, 'S') +// }) +// it('Should remove cloned $id for Array', () => { +// const S = Type.String({ $id: 'S' }) +// const T = Type.Array(S) +// Assert.equal(T.items.$id, undefined) +// Assert.equal(S.$id, 'S') +// }) +// it('Should remove cloned $id for Composite', () => { +// const S = Type.String({ $id: 'S' }) +// const T = Type.Composite([Type.Object({ a: S }), Type.Object({ b: S })]) +// Assert.equal(T.properties.a.$id, undefined) +// Assert.equal(T.properties.b.$id, undefined) +// Assert.equal(S.$id, 'S') +// }) +// it('Should remove cloned $id for Constructor', () => { +// const S = Type.String({ $id: 'S' }) +// const T = Type.Constructor([S], S) +// Assert.equal(T.parameters[0].$id, undefined) +// Assert.equal(T.returns.$id, undefined) +// Assert.equal(S.$id, 'S') +// }) +// it('Should remove cloned $id for Function', () => { +// const S = Type.String({ $id: 'S' }) +// const T = Type.Function([S], S) +// Assert.equal(T.parameters[0].$id, undefined) +// Assert.equal(T.returns.$id, undefined) +// Assert.equal(S.$id, 'S') +// }) +// it('Should remove cloned $id for Intersect', () => { +// const S = Type.String({ $id: 'S' }) +// const T = Type.Intersect([S, S]) +// Assert.equal(T.allOf[0].$id, undefined) +// Assert.equal(T.allOf[1].$id, undefined) +// Assert.equal(S.$id, 'S') +// }) +// it('Should remove cloned $id for Intersect with unevaluatedProperties', () => { +// const S = Type.String({ $id: 'S' }) +// const T = Type.Intersect([S, S], { unevaluatedProperties: S }) +// // @ts-ignore +// Assert.equal(T.unevaluatedProperties.$id, undefined) +// Assert.equal(S.$id, 'S') +// }) +// it('Should remove cloned $id for Not', () => { +// const S = Type.String({ $id: 'S' }) +// const T = Type.Not(S, S) +// Assert.equal(T.allOf[0].not.$id, undefined) +// Assert.equal(T.allOf[1].$id, undefined) +// Assert.equal(S.$id, 'S') +// }) +// it('Should remove cloned $id for Object', () => { +// const S = Type.String({ $id: 'S' }) +// const T = Type.Object({ x: S, y: S }) +// Assert.equal(T.properties.x.$id, undefined) +// Assert.equal(T.properties.y.$id, undefined) +// Assert.equal(S.$id, 'S') +// }) +// it('Should remove cloned $id for nested Object', () => { +// const S = Type.String({ $id: 'S' }) +// const N = Type.Object({ s: S }, { $id: 'N' }) +// const T = Type.Object({ x: S, y: S, z: N }) +// Assert.equal(T.properties.x.$id, undefined) +// Assert.equal(T.properties.y.$id, undefined) +// Assert.equal(T.properties.z.$id, undefined) +// Assert.equal(T.properties.z.properties.s.$id, undefined) +// Assert.equal(S.$id, 'S') +// }) +// it('Should remove cloned $id for Object additionalProperties', () => { +// const S = Type.String({ $id: 'S' }) +// const T = Type.Object({}, { additionalProperties: S }) +// // @ts-ignore +// Assert.equal(T.additionalProperties!.$id, undefined) +// Assert.equal(S.$id, 'S') +// }) +// it('Should remove cloned $id for Promise', () => { +// const S = Type.String({ $id: 'S' }) +// const T = Type.Promise(S) +// Assert.equal(T.item.$id, undefined) +// Assert.equal(S.$id, 'S') +// }) +// it('Should remove cloned $id for Record', () => { +// const S = Type.String({ $id: 'S' }) +// const T = Type.Record(Type.String(), S) +// Assert.equal(T.patternProperties['^.*$'].$id, undefined) +// Assert.equal(S.$id, 'S') +// }) +// it('Should remove cloned $id for Tuple', () => { +// const S = Type.String({ $id: 'S' }) +// const T = Type.Tuple([S, S]) +// Assert.equal(T.items![0]!.$id, undefined) +// Assert.equal(T.items![1]!.$id, undefined) +// Assert.equal(S.$id, 'S') +// }) +// it('Should remove cloned $id for Union', () => { +// const S = Type.String({ $id: 'S' }) +// const T = Type.Union([S, S]) +// Assert.equal(T.anyOf[0]!.$id, undefined) +// Assert.equal(T.anyOf[1]!.$id, undefined) +// Assert.equal(S.$id, 'S') +// }) +// it('Should retain cloned $id for wrapped Recursive 1', () => { +// const S = Type.String({ $id: 'S' }) +// const T = Type.Object({ +// x: Type.Recursive( +// (Self) => +// Type.Object({ +// self: Type.Optional(Self), +// }), +// { $id: 'RecursiveClone' }, +// ), +// }) +// Assert.equal(T.properties.x.$id, 'RecursiveClone') +// Assert.equal(S.$id, 'S') +// }) +// it('Should retain cloned $id for wrapped Recursive 2', () => { +// const S = Type.String({ $id: 'S' }) +// const T = Type.Tuple([ +// Type.Recursive( +// (Self) => +// Type.Object({ +// self: Type.Optional(Self), +// }), +// { $id: 'RecursiveClone' }, +// ), +// ]) +// Assert.equal(T.items![0].$id, 'RecursiveClone') +// Assert.equal(S.$id, 'S') +// }) +// }) diff --git a/test/runtime/type/clone/index.ts b/test/runtime/type/clone/index.ts new file mode 100644 index 000000000..cd798158e --- /dev/null +++ b/test/runtime/type/clone/index.ts @@ -0,0 +1 @@ +import './clone' diff --git a/test/runtime/type/extends/any.ts b/test/runtime/type/extends/any.ts new file mode 100644 index 000000000..6373c9ae0 --- /dev/null +++ b/test/runtime/type/extends/any.ts @@ -0,0 +1,95 @@ +import { Type, ExtendsCheck, ExtendsResult } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('type/extends/Any', () => { + it('Should extend Any', () => { + type T = any extends any ? 1 : 2 + const R = ExtendsCheck(Type.Any(), Type.Any()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Unknown', () => { + type T = any extends unknown ? 1 : 2 + const R = ExtendsCheck(Type.Any(), Type.Unknown()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend String', () => { + type T = any extends string ? 1 : 2 + const R = ExtendsCheck(Type.Any(), Type.String()) + Assert.IsEqual(R, ExtendsResult.Union) + }) + it('Should extend Boolean', () => { + type T = any extends boolean ? 1 : 2 + const R = ExtendsCheck(Type.Any(), Type.Boolean()) + Assert.IsEqual(R, ExtendsResult.Union) + }) + it('Should extend Number', () => { + type T = any extends number ? 1 : 2 + const R = ExtendsCheck(Type.Any(), Type.Number()) + Assert.IsEqual(R, ExtendsResult.Union) + }) + it('Should extend Integer', () => { + type T = any extends number ? 1 : 2 + const R = ExtendsCheck(Type.Any(), Type.Integer()) + Assert.IsEqual(R, ExtendsResult.Union) + }) + it('Should extend Array 1', () => { + type T = any extends Array ? 1 : 2 + const R = ExtendsCheck(Type.Any(), Type.Array(Type.Any())) + Assert.IsEqual(R, ExtendsResult.Union) + }) + it('Should extend Array 2', () => { + type T = any extends Array ? 1 : 2 + const R = ExtendsCheck(Type.Any(), Type.Array(Type.String())) + Assert.IsEqual(R, ExtendsResult.Union) + }) + it('Should extend Tuple', () => { + type T = any extends [number, number] ? 1 : 2 + const R = ExtendsCheck(Type.Any(), Type.Tuple([Type.Number(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.Union) + }) + it('Should extend Object 1', () => { + type T = any extends object ? 1 : 2 + const R = ExtendsCheck(Type.Any(), Type.Object({})) + Assert.IsEqual(R, ExtendsResult.Union) + }) + it('Should extend Object 2', () => { + type T = any extends {} ? 1 : 2 + const R = ExtendsCheck(Type.Any(), Type.Object({})) + Assert.IsEqual(R, ExtendsResult.Union) + }) + it('Should extend Object 3', () => { + type T = any extends { a: number } ? 1 : 2 + const R = ExtendsCheck(Type.Any(), Type.Object({ a: Type.Number() })) + Assert.IsEqual(R, ExtendsResult.Union) + }) + it('Should extend Union 1', () => { + type T = any extends number | string ? 1 : 2 + const R = ExtendsCheck(Type.Any(), Type.Union([Type.Number(), Type.String()])) + Assert.IsEqual(R, ExtendsResult.Union) + }) + it('Should extend Union 2', () => { + type T = any extends any | number ? 1 : 2 + const R = ExtendsCheck(Type.Any(), Type.Union([Type.Any(), Type.String()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Null', () => { + type T = any extends null ? 1 : 2 + const R = ExtendsCheck(Type.Any(), Type.Null()) + Assert.IsEqual(R, ExtendsResult.Union) + }) + it('Should extend Undefined', () => { + type T = any extends undefined ? 1 : 2 + const R = ExtendsCheck(Type.Any(), Type.Undefined()) + Assert.IsEqual(R, ExtendsResult.Union) + }) + it('Should extend Void', () => { + type T = any extends void ? 1 : 2 + const R = ExtendsCheck(Type.Any(), Type.Void()) + Assert.IsEqual(R, ExtendsResult.Union) + }) + it('Should extend Date', () => { + type T = any extends Date ? 1 : 2 + const R = ExtendsCheck(Type.Any(), Type.Date()) + Assert.IsEqual(R, ExtendsResult.Union) + }) +}) diff --git a/test/runtime/type/extends/array.ts b/test/runtime/type/extends/array.ts new file mode 100644 index 000000000..2e468c80c --- /dev/null +++ b/test/runtime/type/extends/array.ts @@ -0,0 +1,264 @@ +import { Type, ExtendsCheck, ExtendsResult } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('type/extends/Array', () => { + // ---------------------------------------------- + // Generic Varying + // ---------------------------------------------- + it('Should extend Array Varying 1', () => { + type T = Array extends Array ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.Any()), Type.Array(Type.Any())) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Array Varying 2', () => { + type T = Array extends Array ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.String()), Type.Array(Type.Any())) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Array Varying 3', () => { + type T = Array extends Array ? 1 : 2 // 1 + const R = ExtendsCheck(Type.Array(Type.Any()), Type.Array(Type.String())) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Array Varying 4', () => { + type T = Array extends Array ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.Number()), Type.Array(Type.String())) + Assert.IsEqual(R, ExtendsResult.False) + }) + // ---------------------------------------------- + // Any + // ---------------------------------------------- + it('Should extend Any', () => { + type T = Array extends any ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.Any()), Type.Any()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Unknown', () => { + type T = Array extends unknown ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.Any()), Type.Unknown()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend String', () => { + type T = Array extends string ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.Any()), Type.String()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Boolean', () => { + type T = Array extends boolean ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.Any()), Type.Boolean()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Number', () => { + type T = Array extends number ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.Any()), Type.Number()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Integer', () => { + type T = Array extends number ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.Any()), Type.Integer()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Array 1', () => { + type T = Array extends Array ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.Any()), Type.Array(Type.Any())) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Array 2', () => { + type T = Array extends Array ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.Any()), Type.Array(Type.Any())) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Array 3', () => { + type T = Array extends Array ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.Any()), Type.Array(Type.Any())) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Tuple', () => { + type T = Array extends [number, number] ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.Any()), Type.Tuple([Type.Number(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Object 1', () => { + type T = Array extends object ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.Any()), Type.Object({})) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Object 2', () => { + type T = Array extends {} ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.Any()), Type.Object({})) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Object 3', () => { + type T = Array extends { a: number } ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.Any()), Type.Object({ a: Type.Number() })) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Object 4', () => { + type T = Array extends { length: '1' } ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.Any()), Type.Object({ length: Type.Literal('1') })) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Object 5', () => { + type T = Array extends { length: number } ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.Any()), Type.Object({ length: Type.Number() })) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 1', () => { + type T = Array extends number | string ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.Any()), Type.Union([Type.Number(), Type.String()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Union 2', () => { + type T = Array extends any | number ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.Any()), Type.Union([Type.Any(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 3', () => { + type T = Array extends any | Array ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.Any()), Type.Union([Type.Any(), Type.Array(Type.Any())])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 4', () => { + type T = Array extends any | Array ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.String()), Type.Union([Type.Any(), Type.Array(Type.Any())])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 5', () => { + type T = Array extends any | Array ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.Any()), Type.Union([Type.Any(), Type.Array(Type.String())])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Null', () => { + type T = Array extends null ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.Any()), Type.Null()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Undefined', () => { + type T = Array extends undefined ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.Any()), Type.Undefined()) + Assert.IsEqual(R, ExtendsResult.False) + }) + // ---------------------------------------------- + // Constrained + // ---------------------------------------------- + it('Should extend constrained Any', () => { + type T = Array extends any ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.String()), Type.Any()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend constrained Unknown', () => { + type T = Array extends unknown ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.String()), Type.Unknown()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend constrained String', () => { + type T = Array extends string ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.String()), Type.String()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend constrained Boolean', () => { + type T = Array extends boolean ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.String()), Type.Boolean()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend constrained Number', () => { + type T = Array extends number ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.String()), Type.Number()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend constrained Integer', () => { + type T = Array extends number ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.String()), Type.Integer()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend constrained Array 1', () => { + type T = Array extends Array ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.String()), Type.Array(Type.Any())) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend constrained Array 2', () => { + type T = Array extends Array ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.String()), Type.Array(Type.Any())) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend constrained Array 3', () => { + type T = Array extends Array ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.String()), Type.Array(Type.Any())) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend constrained Tuple', () => { + type T = Array extends [number, number] ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.String()), Type.Tuple([Type.Number(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend constrained Object 1', () => { + type T = Array extends object ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.String()), Type.Object({})) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend constrained Object 2', () => { + type T = Array extends {} ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.String()), Type.Object({})) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend constrained Object 3', () => { + type T = Array extends { a: number } ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.String()), Type.Object({ a: Type.Number() })) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend constrained Object 4', () => { + type T = Array extends { length: '1' } ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.String()), Type.Object({ length: Type.Literal('1') })) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend constrained Object 5', () => { + type T = Array extends { length: number } ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.String()), Type.Object({ length: Type.Number() })) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend constrained Union 1', () => { + type T = Array extends number | string ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.String()), Type.Union([Type.Null(), Type.String()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend constrained Union 2', () => { + type T = Array extends any | number ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.String()), Type.Union([Type.Any(), Type.String()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend constrained Union 3', () => { + type T = Array extends any | Array ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.String()), Type.Union([Type.Any(), Type.Array(Type.Any())])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend constrained Union 4', () => { + type T = Array extends any | Array ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.String()), Type.Union([Type.Any(), Type.Array(Type.Any())])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend constrained Union 5', () => { + type T = Array extends any | Array ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.String()), Type.Union([Type.Any(), Type.Array(Type.String())])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend constrained Null', () => { + type T = Array extends null ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.String()), Type.Null()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend constrained Undefined', () => { + type T = Array extends undefined ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.String()), Type.Undefined()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend constrained Void', () => { + type T = Array extends void ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.String()), Type.Void()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Date', () => { + type T = Array extends Date ? 1 : 2 + const R = ExtendsCheck(Type.Array(Type.String()), Type.Date()) + Assert.IsEqual(R, ExtendsResult.False) + }) +}) diff --git a/test/runtime/type/extends/async-iterator.ts b/test/runtime/type/extends/async-iterator.ts new file mode 100644 index 000000000..9cc5392a8 --- /dev/null +++ b/test/runtime/type/extends/async-iterator.ts @@ -0,0 +1,71 @@ +import { Type, ExtendsCheck, ExtendsResult } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('type/extends/AsyncIterator', () => { + // ---------------------------------------------- + // Generic Varying + // ---------------------------------------------- + it('Should extend AsyncIterator 1', () => { + type T = AsyncIterableIterator extends AsyncIterableIterator ? 1 : 2 + const R = ExtendsCheck(Type.AsyncIterator(Type.Any()), Type.AsyncIterator(Type.Any())) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend AsyncIterator 2', () => { + type T = AsyncIterableIterator extends AsyncIterableIterator ? 1 : 2 + const R = ExtendsCheck(Type.AsyncIterator(Type.String()), Type.AsyncIterator(Type.String())) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend AsyncIterator 3', () => { + type T = AsyncIterableIterator<'hello'> extends AsyncIterableIterator ? 1 : 2 + const R = ExtendsCheck(Type.AsyncIterator(Type.Literal('hello')), Type.AsyncIterator(Type.String())) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend AsyncIterator 4', () => { + type T = AsyncIterableIterator extends AsyncIterableIterator<'hello'> ? 1 : 2 + const R = ExtendsCheck(Type.AsyncIterator(Type.String()), Type.AsyncIterator(Type.Literal('hello'))) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend AsyncIterator 5', () => { + type T = AsyncIterableIterator extends AsyncIterableIterator<'hello' | number> ? 1 : 2 + const R = ExtendsCheck(Type.AsyncIterator(Type.String()), Type.AsyncIterator(Type.Union([Type.Literal('hello'), Type.Number()]))) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend AsyncIterator 6', () => { + type T = AsyncIterableIterator<'hello' | number> extends AsyncIterableIterator ? 1 : 2 + const R = ExtendsCheck(Type.AsyncIterator(Type.Union([Type.Literal('hello'), Type.Number()])), Type.AsyncIterator(Type.String())) + Assert.IsEqual(R, ExtendsResult.False) + }) + // -------------------------------------------------------------------- + // Structural + // -------------------------------------------------------------------- + it('Should extends Any 1', () => { + type T = AsyncIterableIterator extends any ? 1 : 2 + const R = ExtendsCheck(Type.AsyncIterator(Type.Number()), Type.Any()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extends Any 2', () => { + type T = any extends AsyncIterableIterator ? 1 : 2 + const R = ExtendsCheck(Type.Any(), Type.AsyncIterator(Type.Number())) + Assert.IsEqual(R, ExtendsResult.Union) + }) + it('Should extends Unknown 1', () => { + type T = AsyncIterableIterator extends unknown ? 1 : 2 + const R = ExtendsCheck(Type.AsyncIterator(Type.Number()), Type.Unknown()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extends Unknown 2', () => { + type T = unknown extends AsyncIterableIterator ? 1 : 2 + const R = ExtendsCheck(Type.Unknown(), Type.AsyncIterator(Type.Number())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extends Never 1', () => { + type T = AsyncIterableIterator extends never ? 1 : 2 + const R = ExtendsCheck(Type.AsyncIterator(Type.Number()), Type.Never()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extends Never 2', () => { + type T = never extends AsyncIterableIterator ? 1 : 2 + const R = ExtendsCheck(Type.Never(), Type.AsyncIterator(Type.Number())) + Assert.IsEqual(R, ExtendsResult.True) + }) +}) diff --git a/test/runtime/type/extends/bigint.ts b/test/runtime/type/extends/bigint.ts new file mode 100644 index 000000000..f965eab3d --- /dev/null +++ b/test/runtime/type/extends/bigint.ts @@ -0,0 +1,100 @@ +import { Type, ExtendsCheck, ExtendsResult } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('type/extends/BigInt', () => { + it('Should extend Any', () => { + type T = bigint extends any ? 1 : 2 + const R = ExtendsCheck(Type.BigInt(), Type.Any()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Unknown', () => { + type T = bigint extends unknown ? 1 : 2 + const R = ExtendsCheck(Type.BigInt(), Type.Unknown()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend String', () => { + type T = bigint extends string ? 1 : 2 + const R = ExtendsCheck(Type.BigInt(), Type.String()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Boolean', () => { + type T = bigint extends boolean ? 1 : 2 + const R = ExtendsCheck(Type.BigInt(), Type.Boolean()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Number', () => { + type T = bigint extends number ? 1 : 2 + const R = ExtendsCheck(Type.BigInt(), Type.Number()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Integer', () => { + type T = bigint extends number ? 1 : 2 + const R = ExtendsCheck(Type.BigInt(), Type.Integer()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Array', () => { + type T = bigint extends Array ? 1 : 2 + const R = ExtendsCheck(Type.BigInt(), Type.Array(Type.Any())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Tuple', () => { + type T = bigint extends [number, number] ? 1 : 2 + const R = ExtendsCheck(Type.BigInt(), Type.Tuple([Type.Number(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Record', () => { + type T = bigint extends Record ? 1 : 2 + const R = ExtendsCheck(Type.BigInt(), Type.Record(Type.Number(), Type.Any())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Object 1', () => { + type T = bigint extends {} ? 1 : 2 + const R = ExtendsCheck(Type.BigInt(), Type.Object({})) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Object 2', () => { + type T = bigint extends { a: 10 } ? 1 : 2 + const R = ExtendsCheck(Type.BigInt(), Type.Object({ a: Type.Literal(10) })) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Union 1', () => { + type T = bigint extends number | string ? 1 : 2 + const R = ExtendsCheck(Type.BigInt(), Type.Union([Type.Number(), Type.String()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Union 2', () => { + type T = bigint extends any | number ? 1 : 2 + const R = ExtendsCheck(Type.BigInt(), Type.Union([Type.Any(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 3', () => { + type T = bigint extends boolean | number ? 1 : 2 + const R = ExtendsCheck(Type.BigInt(), Type.Union([Type.Boolean(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Union 4', () => { + type T = bigint extends boolean | bigint ? 1 : 2 + const R = ExtendsCheck(Type.BigInt(), Type.Union([Type.Boolean(), Type.BigInt()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Null', () => { + type T = bigint extends null ? 1 : 2 + const R = ExtendsCheck(Type.BigInt(), Type.Null()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Undefined', () => { + type T = bigint extends undefined ? 1 : 2 + const R = ExtendsCheck(Type.BigInt(), Type.Undefined()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Void', () => { + type T = bigint extends undefined ? 1 : 2 + const R = ExtendsCheck(Type.BigInt(), Type.Void()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Date', () => { + type T = bigint extends Date ? 1 : 2 + const R = ExtendsCheck(Type.BigInt(), Type.Date()) + Assert.IsEqual(R, ExtendsResult.False) + }) +}) diff --git a/test/runtime/type/extends/boolean.ts b/test/runtime/type/extends/boolean.ts new file mode 100644 index 000000000..6e336f331 --- /dev/null +++ b/test/runtime/type/extends/boolean.ts @@ -0,0 +1,90 @@ +import { Type, ExtendsCheck, ExtendsResult } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('type/extends/Boolean', () => { + it('Should extend Any', () => { + type T = boolean extends any ? 1 : 2 + const R = ExtendsCheck(Type.Boolean(), Type.Any()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend String', () => { + type T = boolean extends string ? 1 : 2 + const R = ExtendsCheck(Type.Boolean(), Type.String()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Boolean', () => { + type T = boolean extends boolean ? 1 : 2 + const R = ExtendsCheck(Type.Boolean(), Type.Boolean()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Number', () => { + type T = boolean extends number ? 1 : 2 + const R = ExtendsCheck(Type.Boolean(), Type.Number()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Integer', () => { + type T = boolean extends number ? 1 : 2 + const R = ExtendsCheck(Type.Boolean(), Type.Integer()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Array', () => { + type T = boolean extends Array ? 1 : 2 + const R = ExtendsCheck(Type.Boolean(), Type.Array(Type.Any())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Tuple', () => { + type T = boolean extends [number, number] ? 1 : 2 + const R = ExtendsCheck(Type.Boolean(), Type.Tuple([Type.Number(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Record', () => { + type T = boolean extends Record ? 1 : 2 + const R = ExtendsCheck(Type.Number(), Type.Record(Type.Number(), Type.Any())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Object 1', () => { + type T = boolean extends {} ? 1 : 2 + const R = ExtendsCheck(Type.Boolean(), Type.Object({}, { additionalProperties: false })) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Object 2', () => { + type T = boolean extends { a: 10 } ? 1 : 2 + const R = ExtendsCheck(Type.Boolean(), Type.Object({ a: Type.Literal(10) }, { additionalProperties: true })) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Union 1', () => { + type T = boolean extends number | string ? 1 : 2 + const R = ExtendsCheck(Type.Boolean(), Type.Union([Type.Number(), Type.String()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Union 2', () => { + type T = boolean extends any | number ? 1 : 2 + const R = ExtendsCheck(Type.Boolean(), Type.Union([Type.Any(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 2', () => { + type T = boolean extends boolean | number ? 1 : 2 + const R = ExtendsCheck(Type.Boolean(), Type.Union([Type.Boolean(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Null', () => { + type T = boolean extends null ? 1 : 2 + const R = ExtendsCheck(Type.Boolean(), Type.Null()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Undefined', () => { + type T = boolean extends undefined ? 1 : 2 + const R = ExtendsCheck(Type.Boolean(), Type.Undefined()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Void', () => { + type T = boolean extends void ? 1 : 2 + const R = ExtendsCheck(Type.Boolean(), Type.Void()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Date', () => { + type T = boolean extends Date ? 1 : 2 + const R = ExtendsCheck(Type.Boolean(), Type.Date()) + Assert.IsEqual(R, ExtendsResult.False) + }) +}) diff --git a/test/runtime/type/extends/constructor.ts b/test/runtime/type/extends/constructor.ts new file mode 100644 index 000000000..a590c842f --- /dev/null +++ b/test/runtime/type/extends/constructor.ts @@ -0,0 +1,210 @@ +import { Type, ExtendsCheck, ExtendsResult } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('type/extends/Constructor', () => { + it('Should extend Function', () => { + type T = (new () => number) extends () => number ? 1 : 2 + const R = ExtendsCheck(Type.Constructor([], Type.Number()), Type.Function([], Type.Number())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Constructor 1', () => { + type T = (new () => number) extends new () => number ? 1 : 2 + const R = ExtendsCheck(Type.Constructor([], Type.Number()), Type.Constructor([], Type.Number())) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Constructor 2', () => { + type T = (new () => any) extends new () => number ? 1 : 2 + const R = ExtendsCheck(Type.Constructor([], Type.Any()), Type.Constructor([], Type.Number())) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Constructor 3', () => { + type T = (new () => number) extends new () => any ? 1 : 2 + const R = ExtendsCheck(Type.Constructor([], Type.Any()), Type.Constructor([], Type.Number())) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Constructor 4', () => { + type T = (new (a: number) => number) extends new () => any ? 1 : 2 + const R = ExtendsCheck(Type.Constructor([Type.Number()], Type.Number()), Type.Constructor([], Type.Any())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Constructor 5', () => { + type T = (new (a: number | string) => number) extends new (a: number) => number ? 1 : 2 + const R = ExtendsCheck(Type.Constructor([Type.Union([Type.Number(), Type.String()])], Type.Number()), Type.Constructor([Type.Number()], Type.Any())) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Constructor 6', () => { + type T = (new (a: number) => number) extends new (a: number | string) => any ? 1 : 2 + const R = ExtendsCheck(Type.Constructor([Type.Number()], Type.Number()), Type.Constructor([Type.Union([Type.Number(), Type.String()])], Type.Any())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Constructor 7', () => { + type T = (new (a: number, b: number) => number) extends new (a: number) => number ? 1 : 2 + const R = ExtendsCheck(Type.Constructor([Type.Number(), Type.Number()], Type.Number()), Type.Constructor([Type.Number()], Type.Number())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Constructor 8', () => { + type T = (new (a: number) => number) extends new (a: number, b: number) => number ? 1 : 2 + const R = ExtendsCheck(Type.Constructor([Type.Number()], Type.Number()), Type.Constructor([Type.Number(), Type.Number()], Type.Number())) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Constructor 9', () => { + type T = (new () => number) extends new () => any ? 1 : 2 + const R = ExtendsCheck(Type.Constructor([], Type.Number()), Type.Constructor([], Type.Any())) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Constructor 9', () => { + type T = (new () => any) extends new () => number ? 1 : 2 + const R = ExtendsCheck(Type.Constructor([], Type.Any()), Type.Constructor([], Type.Number())) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Constructor 10', () => { + type T = (new () => Array) extends new () => object ? 1 : 2 + const R = ExtendsCheck(Type.Constructor([], Type.Array(Type.Any())), Type.Constructor([], Type.Object({}))) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Constructor 11', () => { + type T = (new () => Array) extends new () => object ? 1 : 2 + const R = ExtendsCheck(Type.Constructor([], Type.Array(Type.String())), Type.Constructor([], Type.Object({}))) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Constructor 12', () => { + type T = (new () => object) extends new () => Array ? 1 : 2 + const R = ExtendsCheck(Type.Constructor([], Type.Object({})), Type.Constructor([], Type.Array(Type.Any()))) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Constructor 13', () => { + type T = (new (a: unknown) => number) extends new (a: any) => number ? 1 : 2 + const R = ExtendsCheck(Type.Constructor([Type.Unknown()], Type.Number({})), Type.Constructor([Type.Any()], Type.Number({}))) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Constructor 14', () => { + type T = (new (a: any) => number) extends new (a: unknown) => number ? 1 : 2 + const R = ExtendsCheck(Type.Constructor([Type.Any()], Type.Number({})), Type.Constructor([Type.Unknown()], Type.Number({}))) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Constructor 15', () => { + type T = (new () => any) extends new () => unknown ? 1 : 2 + const R = ExtendsCheck(Type.Constructor([], Type.Any({})), Type.Constructor([], Type.Unknown({}))) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Constructor 16', () => { + type T = (new () => unknown) extends new () => any ? 1 : 2 + const R = ExtendsCheck(Type.Constructor([], Type.Unknown({})), Type.Constructor([], Type.Any({}))) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Any', () => { + type T = (new () => number) extends any ? 1 : 2 + const R = ExtendsCheck(Type.Constructor([], Type.Number()), Type.Any()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend String', () => { + type T = (new () => number) extends string ? 1 : 2 + const R = ExtendsCheck(Type.Constructor([], Type.Number()), Type.String()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Boolean', () => { + type T = (new () => number) extends boolean ? 1 : 2 + const R = ExtendsCheck(Type.Constructor([], Type.Number()), Type.Boolean()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Number', () => { + type T = (new () => number) extends number ? 1 : 2 + const R = ExtendsCheck(Type.Constructor([], Type.Number()), Type.Number()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Integer', () => { + type T = (new () => number) extends number ? 1 : 2 + const R = ExtendsCheck(Type.Constructor([], Type.Number()), Type.Integer()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Array 1', () => { + type T = (new () => number) extends Array ? 1 : 2 + const R = ExtendsCheck(Type.Constructor([], Type.Number()), Type.Array(Type.Any())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Array 2', () => { + type T = (new () => number) extends Array ? 1 : 2 + const R = ExtendsCheck(Type.Constructor([], Type.Number()), Type.Array(Type.Any())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Array 3', () => { + type T = (new () => number) extends Array ? 1 : 2 + const R = ExtendsCheck(Type.Constructor([], Type.Number()), Type.Array(Type.Any())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Tuple', () => { + type T = (new () => number) extends [number, number] ? 1 : 2 + const R = ExtendsCheck(Type.Constructor([], Type.Number()), Type.Tuple([Type.Number(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Record', () => { + type T = (() => number) extends Record ? 1 : 2 + const R = ExtendsCheck(Type.Constructor([], Type.Number()), Type.Record(Type.Number(), Type.Any())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Object 1', () => { + type T = (new () => number) extends object ? 1 : 2 + const R = ExtendsCheck(Type.Constructor([], Type.Number()), Type.Object({})) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Object 2', () => { + type T = (new () => number) extends {} ? 1 : 2 + const R = ExtendsCheck(Type.Constructor([], Type.Number()), Type.Object({})) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Object 3', () => { + type T = (new () => number) extends { a: number } ? 1 : 2 + const R = ExtendsCheck(Type.Constructor([], Type.Number()), Type.Object({ a: Type.Number() })) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Object 4', () => { + type T = (new () => number) extends { length: '1' } ? 1 : 2 + const R = ExtendsCheck(Type.Constructor([], Type.Number()), Type.Object({ length: Type.Literal('1') })) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Union 1', () => { + type T = (new () => number) extends number | string ? 1 : 2 + const R = ExtendsCheck(Type.Constructor([], Type.Number()), Type.Union([Type.Null(), Type.String()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Union 2', () => { + type T = (new () => number) extends any | number ? 1 : 2 + const R = ExtendsCheck(Type.Constructor([], Type.Number()), Type.Union([Type.Any(), Type.String()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 3', () => { + type T = (new () => number) extends any | Array ? 1 : 2 + const R = ExtendsCheck(Type.Constructor([], Type.Number()), Type.Union([Type.Any(), Type.Array(Type.Any())])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 4', () => { + type T = (new () => number) extends any | Array ? 1 : 2 + const R = ExtendsCheck(Type.Constructor([], Type.Number()), Type.Union([Type.Any(), Type.Array(Type.Any())])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 5', () => { + type T = (new () => number) extends any | Array ? 1 : 2 + const R = ExtendsCheck(Type.Constructor([], Type.Number()), Type.Union([Type.Any(), Type.Array(Type.String())])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Null', () => { + type T = (new () => number) extends null ? 1 : 2 + const R = ExtendsCheck(Type.Constructor([], Type.Number()), Type.Null()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Undefined', () => { + type T = (new () => number) extends undefined ? 1 : 2 + const R = ExtendsCheck(Type.Constructor([], Type.Number()), Type.Undefined()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Void', () => { + type T = (new () => number) extends void ? 1 : 2 + const R = ExtendsCheck(Type.Constructor([], Type.Number()), Type.Void()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Date', () => { + type T = (new () => number) extends Date ? 1 : 2 + const R = ExtendsCheck(Type.Constructor([], Type.Number()), Type.Date()) + Assert.IsEqual(R, ExtendsResult.False) + }) +}) diff --git a/test/runtime/type/extends/date.ts b/test/runtime/type/extends/date.ts new file mode 100644 index 000000000..2bcfa8864 --- /dev/null +++ b/test/runtime/type/extends/date.ts @@ -0,0 +1,95 @@ +import { Type, ExtendsCheck, ExtendsResult } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('type/extends/Date', () => { + it('Should extend Any', () => { + type T = Date extends any ? 1 : 2 + const R = ExtendsCheck(Type.Date(), Type.Any()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Unknown', () => { + type T = Date extends unknown ? 1 : 2 + const R = ExtendsCheck(Type.Date(), Type.Unknown()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend String', () => { + type T = Date extends string ? 1 : 2 + const R = ExtendsCheck(Type.Date(), Type.String()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Boolean', () => { + type T = Date extends boolean ? 1 : 2 + const R = ExtendsCheck(Type.Date(), Type.Boolean()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Number', () => { + type T = Date extends number ? 1 : 2 + const R = ExtendsCheck(Type.Date(), Type.Number()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Integer', () => { + type T = Date extends number ? 1 : 2 + const R = ExtendsCheck(Type.Date(), Type.Integer()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Array', () => { + type T = Date extends Array ? 1 : 2 + const R = ExtendsCheck(Type.Date(), Type.Array(Type.Any())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Tuple', () => { + type T = Date extends [number, number] ? 1 : 2 + const R = ExtendsCheck(Type.Date(), Type.Tuple([Type.Number(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Record', () => { + type T = Date extends Record ? 1 : 2 + const R = ExtendsCheck(Type.Date(), Type.Record(Type.Number(), Type.Any())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Object 1', () => { + type T = Date extends {} ? 1 : 2 + const R = ExtendsCheck(Type.Date(), Type.Object({})) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Object 2', () => { + type T = Date extends { a: 10 } ? 1 : 2 + const R = ExtendsCheck(Type.Date(), Type.Object({ a: Type.Literal(10) }, { additionalProperties: true })) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Union 1', () => { + type T = Date extends number | string ? 1 : 2 + const R = ExtendsCheck(Type.Date(), Type.Union([Type.Number(), Type.String()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Union 2', () => { + type T = Date extends any | number ? 1 : 2 + const R = ExtendsCheck(Type.Date(), Type.Union([Type.Any(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 3', () => { + type T = Date extends boolean | number ? 1 : 2 + const R = ExtendsCheck(Type.Date(), Type.Union([Type.Boolean(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Null', () => { + type T = Date extends null ? 1 : 2 + const R = ExtendsCheck(Type.Date(), Type.Null()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Undefined', () => { + type T = Date extends undefined ? 1 : 2 + const R = ExtendsCheck(Type.Date(), Type.Undefined()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Void', () => { + type T = Date extends void ? 1 : 2 + const R = ExtendsCheck(Type.Date(), Type.Void()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Date', () => { + type T = Date extends Date ? 1 : 2 + const R = ExtendsCheck(Type.Date(), Type.Date()) + Assert.IsEqual(R, ExtendsResult.True) + }) +}) diff --git a/test/runtime/type/extends/function.ts b/test/runtime/type/extends/function.ts new file mode 100644 index 000000000..e889555a3 --- /dev/null +++ b/test/runtime/type/extends/function.ts @@ -0,0 +1,210 @@ +import { Type, ExtendsCheck, ExtendsResult } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('type/extends/Function', () => { + it('Should extend Constructor 1', () => { + type T = (() => number) extends new () => number ? 1 : 2 + const R = ExtendsCheck(Type.Function([], Type.Number()), Type.Constructor([], Type.Number())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Function 1', () => { + type T = (() => number) extends () => number ? 1 : 2 + const R = ExtendsCheck(Type.Function([], Type.Number()), Type.Function([], Type.Number())) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Function 2', () => { + type T = (() => any) extends () => number ? 1 : 2 + const R = ExtendsCheck(Type.Function([], Type.Any()), Type.Function([], Type.Number())) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Function 3', () => { + type T = (() => number) extends () => any ? 1 : 2 + const R = ExtendsCheck(Type.Function([], Type.Any()), Type.Function([], Type.Number())) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Function 4', () => { + type T = ((a: number) => number) extends () => any ? 1 : 2 + const R = ExtendsCheck(Type.Function([Type.Number()], Type.Number()), Type.Function([], Type.Any())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Function 5', () => { + type T = ((a: number | string) => number) extends (a: number) => number ? 1 : 2 + const R = ExtendsCheck(Type.Function([Type.Union([Type.Number(), Type.String()])], Type.Number()), Type.Function([Type.Number()], Type.Any())) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Function 6', () => { + type T = ((a: number) => number) extends (a: number | string) => any ? 1 : 2 + const R = ExtendsCheck(Type.Function([Type.Number()], Type.Number()), Type.Function([Type.Union([Type.Number(), Type.String()])], Type.Any())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Function 7', () => { + type T = ((a: number, b: number) => number) extends (a: number) => number ? 1 : 2 + const R = ExtendsCheck(Type.Function([Type.Number(), Type.Number()], Type.Number()), Type.Function([Type.Number()], Type.Number())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Function 8', () => { + type T = ((a: number) => number) extends (a: number, b: number) => number ? 1 : 2 + const R = ExtendsCheck(Type.Function([Type.Number()], Type.Number()), Type.Function([Type.Number(), Type.Number()], Type.Number())) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Function 9', () => { + type T = (() => number) extends () => any ? 1 : 2 + const R = ExtendsCheck(Type.Function([], Type.Number()), Type.Function([], Type.Any())) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Function 9', () => { + type T = (() => any) extends () => number ? 1 : 2 + const R = ExtendsCheck(Type.Function([], Type.Any()), Type.Function([], Type.Number())) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Function 10', () => { + type T = (() => Array) extends () => object ? 1 : 2 + const R = ExtendsCheck(Type.Function([], Type.Array(Type.Any())), Type.Function([], Type.Object({}))) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Function 11', () => { + type T = (() => Array) extends () => object ? 1 : 2 + const R = ExtendsCheck(Type.Function([], Type.Array(Type.String())), Type.Function([], Type.Object({}))) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Function 12', () => { + type T = (() => object) extends () => Array ? 1 : 2 + const R = ExtendsCheck(Type.Function([], Type.Object({})), Type.Function([], Type.Array(Type.Any()))) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Function 13', () => { + type T = ((a: unknown) => number) extends (a: any) => number ? 1 : 2 + const R = ExtendsCheck(Type.Function([Type.Unknown()], Type.Number({})), Type.Function([Type.Any()], Type.Number({}))) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Function 14', () => { + type T = ((a: any) => number) extends (a: unknown) => number ? 1 : 2 + const R = ExtendsCheck(Type.Function([Type.Any()], Type.Number({})), Type.Function([Type.Unknown()], Type.Number({}))) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Function 15', () => { + type T = (() => any) extends () => unknown ? 1 : 2 + const R = ExtendsCheck(Type.Function([], Type.Any({})), Type.Function([], Type.Unknown({}))) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Function 16', () => { + type T = (() => unknown) extends () => any ? 1 : 2 + const R = ExtendsCheck(Type.Function([], Type.Unknown({})), Type.Function([], Type.Any({}))) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Any', () => { + type T = (() => number) extends any ? 1 : 2 + const R = ExtendsCheck(Type.Function([], Type.Number()), Type.Any()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend String', () => { + type T = (() => number) extends string ? 1 : 2 + const R = ExtendsCheck(Type.Function([], Type.Number()), Type.String()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Boolean', () => { + type T = (() => number) extends boolean ? 1 : 2 + const R = ExtendsCheck(Type.Function([], Type.Number()), Type.Boolean()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Number', () => { + type T = (() => number) extends number ? 1 : 2 + const R = ExtendsCheck(Type.Function([], Type.Number()), Type.Number()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Integer', () => { + type T = (() => number) extends number ? 1 : 2 + const R = ExtendsCheck(Type.Function([], Type.Number()), Type.Integer()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Array 1', () => { + type T = (() => number) extends Array ? 1 : 2 + const R = ExtendsCheck(Type.Function([], Type.Number()), Type.Array(Type.Any())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Array 2', () => { + type T = (() => number) extends Array ? 1 : 2 + const R = ExtendsCheck(Type.Function([], Type.Number()), Type.Array(Type.Any())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Array 3', () => { + type T = (() => number) extends Array ? 1 : 2 + const R = ExtendsCheck(Type.Function([], Type.Number()), Type.Array(Type.Any())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Tuple', () => { + type T = (() => number) extends [number, number] ? 1 : 2 + const R = ExtendsCheck(Type.Function([], Type.Number()), Type.Tuple([Type.Number(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Record', () => { + type T = (() => number) extends Record ? 1 : 2 + const R = ExtendsCheck(Type.Function([], Type.Number()), Type.Record(Type.Number(), Type.Any())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Object 1', () => { + type T = (() => number) extends object ? 1 : 2 + const R = ExtendsCheck(Type.Function([], Type.Number()), Type.Object({})) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Object 2', () => { + type T = (() => number) extends {} ? 1 : 2 + const R = ExtendsCheck(Type.Function([], Type.Number()), Type.Object({})) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Object 3', () => { + type T = (() => number) extends { a: number } ? 1 : 2 + const R = ExtendsCheck(Type.Function([], Type.Number()), Type.Object({ a: Type.Number() })) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Object 4', () => { + type T = (() => number) extends { length: '1' } ? 1 : 2 + const R = ExtendsCheck(Type.Function([], Type.Number()), Type.Object({ length: Type.Literal('1') })) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Object 5', () => { + type T = (() => number) extends { length: number } ? 1 : 2 + const R = ExtendsCheck(Type.Function([], Type.Number()), Type.Object({ length: Type.Number() })) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 1', () => { + type T = (() => number) extends number | string ? 1 : 2 + const R = ExtendsCheck(Type.Function([], Type.Number()), Type.Union([Type.Null(), Type.String()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Union 2', () => { + type T = (() => number) extends any | number ? 1 : 2 + const R = ExtendsCheck(Type.Function([], Type.Number()), Type.Union([Type.Any(), Type.String()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 3', () => { + type T = (() => number) extends any | Array ? 1 : 2 + const R = ExtendsCheck(Type.Function([], Type.Number()), Type.Union([Type.Any(), Type.Array(Type.Any())])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 4', () => { + type T = (() => number) extends any | Array ? 1 : 2 + const R = ExtendsCheck(Type.Function([], Type.Number()), Type.Union([Type.Any(), Type.Array(Type.Any())])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 5', () => { + type T = (() => number) extends any | Array ? 1 : 2 + const R = ExtendsCheck(Type.Function([], Type.Number()), Type.Union([Type.Any(), Type.Array(Type.String())])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Null', () => { + type T = (() => number) extends null ? 1 : 2 + const R = ExtendsCheck(Type.Function([], Type.Number()), Type.Null()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Void', () => { + type T = (() => number) extends undefined ? 1 : 2 + const R = ExtendsCheck(Type.Function([], Type.Number()), Type.Void()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Date', () => { + type T = (() => number) extends Date ? 1 : 2 + const R = ExtendsCheck(Type.Constructor([], Type.Number()), Type.Date()) + Assert.IsEqual(R, ExtendsResult.False) + }) +}) diff --git a/test/runtime/type/extends/index.ts b/test/runtime/type/extends/index.ts new file mode 100644 index 000000000..033113275 --- /dev/null +++ b/test/runtime/type/extends/index.ts @@ -0,0 +1,27 @@ +import './any' +import './array' +import './async-iterator' +import './bigint' +import './boolean' +import './constructor' +import './date' +import './function' +import './integer' +import './iterator' +import './literal' +import './not' +import './null' +import './number' +import './object' +import './promise' +import './record' +import './regexp' +import './string' +import './symbol' +import './template-literal' +import './tuple' +import './uint8array' +import './undefined' +import './union' +import './unknown' +import './void' diff --git a/test/runtime/type/extends/integer.ts b/test/runtime/type/extends/integer.ts new file mode 100644 index 000000000..6abd57529 --- /dev/null +++ b/test/runtime/type/extends/integer.ts @@ -0,0 +1,95 @@ +import { Type, ExtendsCheck, ExtendsResult } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('type/extends/Integer', () => { + it('Should extend Any', () => { + type T = number extends any ? 1 : 2 + const R = ExtendsCheck(Type.Integer(), Type.Any()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Unknown', () => { + type T = number extends unknown ? 1 : 2 + const R = ExtendsCheck(Type.Integer(), Type.Unknown()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend String', () => { + type T = number extends string ? 1 : 2 + const R = ExtendsCheck(Type.Integer(), Type.String()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Boolean', () => { + type T = number extends boolean ? 1 : 2 + const R = ExtendsCheck(Type.Integer(), Type.Boolean()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Number', () => { + type T = number extends number ? 1 : 2 + const R = ExtendsCheck(Type.Integer(), Type.Number()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Integer', () => { + type T = number extends number ? 1 : 2 + const R = ExtendsCheck(Type.Integer(), Type.Integer()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Array', () => { + type T = number extends Array ? 1 : 2 + const R = ExtendsCheck(Type.Integer(), Type.Array(Type.Any())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Tuple', () => { + type T = number extends [number, number] ? 1 : 2 + const R = ExtendsCheck(Type.Integer(), Type.Tuple([Type.Number(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Record', () => { + type T = number extends Record ? 1 : 2 + const R = ExtendsCheck(Type.Integer(), Type.Record(Type.Number(), Type.Any())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Object 1', () => { + type T = number extends {} ? 1 : 2 + const R = ExtendsCheck(Type.Integer(), Type.Object({})) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Object 2', () => { + type T = number extends { a: 10 } ? 1 : 2 + const R = ExtendsCheck(Type.Integer(), Type.Object({ a: Type.Literal(10) })) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Union 1', () => { + type T = number extends number | string ? 1 : 2 + const R = ExtendsCheck(Type.Integer(), Type.Union([Type.Number(), Type.String()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 2', () => { + type T = number extends any | number ? 1 : 2 + const R = ExtendsCheck(Type.Integer(), Type.Union([Type.Any(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 2', () => { + type T = number extends boolean | number ? 1 : 2 + const R = ExtendsCheck(Type.Integer(), Type.Union([Type.Boolean(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Null', () => { + type T = number extends null ? 1 : 2 + const R = ExtendsCheck(Type.Integer(), Type.Null()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Undefined', () => { + type T = number extends undefined ? 1 : 2 + const R = ExtendsCheck(Type.Integer(), Type.Undefined()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Void', () => { + type T = number extends undefined ? 1 : 2 + const R = ExtendsCheck(Type.Integer(), Type.Void()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Date', () => { + type T = number extends Date ? 1 : 2 + const R = ExtendsCheck(Type.Number(), Type.Date()) + Assert.IsEqual(R, ExtendsResult.False) + }) +}) diff --git a/test/runtime/type/extends/iterator.ts b/test/runtime/type/extends/iterator.ts new file mode 100644 index 000000000..d685dfab1 --- /dev/null +++ b/test/runtime/type/extends/iterator.ts @@ -0,0 +1,71 @@ +import { Type, ExtendsCheck, ExtendsResult } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('type/extends/Iterator', () => { + // ---------------------------------------------- + // Generic Varying + // ---------------------------------------------- + it('Should extend Iterator 1', () => { + type T = IterableIterator extends IterableIterator ? 1 : 2 + const R = ExtendsCheck(Type.Iterator(Type.Any()), Type.Iterator(Type.Any())) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Iterator 2', () => { + type T = IterableIterator extends IterableIterator ? 1 : 2 + const R = ExtendsCheck(Type.Iterator(Type.String()), Type.Iterator(Type.String())) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Iterator 3', () => { + type T = IterableIterator<'hello'> extends IterableIterator ? 1 : 2 + const R = ExtendsCheck(Type.Iterator(Type.Literal('hello')), Type.Iterator(Type.String())) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Iterator 4', () => { + type T = IterableIterator extends IterableIterator<'hello'> ? 1 : 2 + const R = ExtendsCheck(Type.Iterator(Type.String()), Type.Iterator(Type.Literal('hello'))) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Iterator 5', () => { + type T = IterableIterator extends IterableIterator<'hello' | number> ? 1 : 2 + const R = ExtendsCheck(Type.Iterator(Type.String()), Type.Iterator(Type.Union([Type.Literal('hello'), Type.Number()]))) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Iterator 6', () => { + type T = IterableIterator<'hello' | number> extends IterableIterator ? 1 : 2 + const R = ExtendsCheck(Type.Iterator(Type.Union([Type.Literal('hello'), Type.Number()])), Type.Iterator(Type.String())) + Assert.IsEqual(R, ExtendsResult.False) + }) + // -------------------------------------------------------------------- + // Structural + // -------------------------------------------------------------------- + it('Should extends Any 1', () => { + type T = IterableIterator extends any ? 1 : 2 + const R = ExtendsCheck(Type.Iterator(Type.Number()), Type.Any()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extends Any 2', () => { + type T = any extends IterableIterator ? 1 : 2 + const R = ExtendsCheck(Type.Any(), Type.Iterator(Type.Number())) + Assert.IsEqual(R, ExtendsResult.Union) + }) + it('Should extends Unknown 1', () => { + type T = IterableIterator extends unknown ? 1 : 2 + const R = ExtendsCheck(Type.Iterator(Type.Number()), Type.Unknown()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extends Unknown 2', () => { + type T = unknown extends IterableIterator ? 1 : 2 + const R = ExtendsCheck(Type.Unknown(), Type.Iterator(Type.Number())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extends Never 1', () => { + type T = IterableIterator extends never ? 1 : 2 + const R = ExtendsCheck(Type.Iterator(Type.Number()), Type.Never()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extends Never 2', () => { + type T = never extends IterableIterator ? 1 : 2 + const R = ExtendsCheck(Type.Never(), Type.Iterator(Type.Number())) + Assert.IsEqual(R, ExtendsResult.True) + }) +}) diff --git a/test/runtime/type/extends/literal.ts b/test/runtime/type/extends/literal.ts new file mode 100644 index 000000000..ca2c952b6 --- /dev/null +++ b/test/runtime/type/extends/literal.ts @@ -0,0 +1,264 @@ +import { Type, ExtendsCheck, ExtendsResult } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('type/extends/Literal', () => { + // ------------------------------------------------------------------- + // String Literal + // ------------------------------------------------------------------- + it('Should extend Any (String)', () => { + type T = 'hello' extends any ? 1 : 2 + const R = ExtendsCheck(Type.Literal('hello'), Type.Any()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Unknown (String)', () => { + type T = 'hello' extends unknown ? 1 : 2 + const R = ExtendsCheck(Type.Literal('hello'), Type.Unknown()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend String (String)', () => { + type T = 'hello' extends string ? 1 : 2 + const R = ExtendsCheck(Type.Literal('hello'), Type.String()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Boolean (String)', () => { + type T = 'hello' extends boolean ? 1 : 2 + const R = ExtendsCheck(Type.Literal('hello'), Type.Boolean()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Number (String)', () => { + type T = 'hello' extends number ? 1 : 2 + const R = ExtendsCheck(Type.Literal('hello'), Type.Number()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Integer (String)', () => { + type T = 'hello' extends number ? 1 : 2 + const R = ExtendsCheck(Type.Literal('hello'), Type.Integer()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Array (String)', () => { + type T = 'hello' extends Array ? 1 : 2 + const R = ExtendsCheck(Type.Literal('hello'), Type.Array(Type.Any())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Tuple (String)', () => { + type T = 'hello' extends [number, number] ? 1 : 2 + const R = ExtendsCheck(Type.Literal('hello'), Type.Tuple([Type.Number(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Object 1 (String)', () => { + type T = 'hello' extends {} ? 1 : 2 + const R = ExtendsCheck(Type.Literal('hello'), Type.Object({}, { additionalProperties: false })) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Object 2 (String)', () => { + type T = 'hello' extends { a: 10 } ? 1 : 2 + const R = ExtendsCheck(Type.Literal('hello'), Type.Object({ a: Type.Literal(10) }, { additionalProperties: true })) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Union 1 (String)', () => { + type T = 'hello' extends number | string ? 1 : 2 + const R = ExtendsCheck(Type.Literal('hello'), Type.Union([Type.Number(), Type.String()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 2 (String)', () => { + type T = 'hello' extends any | number ? 1 : 2 + const R = ExtendsCheck(Type.Literal('hello'), Type.Union([Type.Any(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 3 (String)', () => { + type T = 'hello' extends boolean | number ? 1 : 2 + const R = ExtendsCheck(Type.Literal('hello'), Type.Union([Type.Boolean(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Null (String)', () => { + type T = 'hello' extends null ? 1 : 2 + const R = ExtendsCheck(Type.Literal('hello'), Type.Null()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Undefined (String)', () => { + type T = 'hello' extends undefined ? 1 : 2 + const R = ExtendsCheck(Type.Literal('hello'), Type.Undefined()) + Assert.IsEqual(R, ExtendsResult.False) + }) + // ------------------------------------------------------------------- + // Number Literal + // ------------------------------------------------------------------- + it('Should extend Any (Number)', () => { + type T = 10 extends any ? 1 : 2 + const R = ExtendsCheck(Type.Literal(10), Type.Any()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Unknown (Number)', () => { + type T = 10 extends unknown ? 1 : 2 + const R = ExtendsCheck(Type.Literal(10), Type.Unknown()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend String (Number)', () => { + type T = 10 extends string ? 1 : 2 + const R = ExtendsCheck(Type.Literal(10), Type.String()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Boolean (Number)', () => { + type T = 10 extends boolean ? 1 : 2 + const R = ExtendsCheck(Type.Literal(10), Type.Boolean()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Number (Number)', () => { + type T = 10 extends number ? 1 : 2 + const R = ExtendsCheck(Type.Literal(10), Type.Number()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Integer (Number)', () => { + type T = 10 extends number ? 1 : 2 + const R = ExtendsCheck(Type.Literal(10), Type.Integer()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Array (Number)', () => { + type T = 10 extends Array ? 1 : 2 + const R = ExtendsCheck(Type.Literal(10), Type.Array(Type.Any())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Tuple (Number)', () => { + type T = 10 extends [number, number] ? 1 : 2 + const R = ExtendsCheck(Type.Literal(10), Type.Tuple([Type.Number(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Object 1 (Number)', () => { + type T = 10 extends {} ? 1 : 2 + const R = ExtendsCheck(Type.Literal(10), Type.Object({}, { additionalProperties: false })) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Object 2 (Number)', () => { + type T = 10 extends { a: 10 } ? 1 : 2 + const R = ExtendsCheck(Type.Literal(10), Type.Object({ a: Type.Literal(10) }, { additionalProperties: true })) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Union 1 (Number)', () => { + type T = 10 extends number | string ? 1 : 2 + const R = ExtendsCheck(Type.Literal(10), Type.Union([Type.Number(), Type.String()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 2 (Number)', () => { + type T = 10 extends any | number ? 1 : 2 + const R = ExtendsCheck(Type.Literal(10), Type.Union([Type.Any(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 3 (Number)', () => { + type T = 10 extends boolean | number ? 1 : 2 + const R = ExtendsCheck(Type.Literal(10), Type.Union([Type.Boolean(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Null (Number)', () => { + type T = 10 extends null ? 1 : 2 + const R = ExtendsCheck(Type.Literal(10), Type.Null()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Undefined (Number)', () => { + type T = 10 extends undefined ? 1 : 2 + const R = ExtendsCheck(Type.Literal(10), Type.Undefined()) + Assert.IsEqual(R, ExtendsResult.False) + }) + // ------------------------------------------------------------------- + // Boolean Literal + // ------------------------------------------------------------------- + it('Should extend Any (Boolean)', () => { + type T = true extends any ? 1 : 2 + const R = ExtendsCheck(Type.Literal(true), Type.Any()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Unknown (Boolean)', () => { + type T = true extends unknown ? 1 : 2 + const R = ExtendsCheck(Type.Literal(true), Type.Unknown()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend String (Boolean)', () => { + type T = true extends string ? 1 : 2 + const R = ExtendsCheck(Type.Literal(true), Type.String()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Boolean (Boolean)', () => { + type T = true extends boolean ? 1 : 2 + const R = ExtendsCheck(Type.Literal(true), Type.Boolean()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Number (Boolean)', () => { + type T = true extends number ? 1 : 2 + const R = ExtendsCheck(Type.Literal(true), Type.Number()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Integer (Boolean)', () => { + type T = true extends number ? 1 : 2 + const R = ExtendsCheck(Type.Literal(true), Type.Integer()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Array (Boolean)', () => { + type T = true extends Array ? 1 : 2 + const R = ExtendsCheck(Type.Literal(true), Type.Array(Type.Any())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Tuple (Boolean)', () => { + type T = true extends [number, number] ? 1 : 2 + const R = ExtendsCheck(Type.Literal(true), Type.Tuple([Type.Number(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Record 1', () => { + type T = 'hello' extends Record ? 1 : 2 + const R = ExtendsCheck(Type.Literal('hello'), Type.Record(Type.Number(), Type.Any())) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Record 2', () => { + type T = 10 extends Record ? 1 : 2 + const R = ExtendsCheck(Type.Literal(10), Type.Record(Type.Number(), Type.Any())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Record 3', () => { + type T = true extends Record ? 1 : 2 + const R = ExtendsCheck(Type.Literal(true), Type.Record(Type.Number(), Type.Any())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Object 1 (Boolean)', () => { + type T = true extends {} ? 1 : 2 + const R = ExtendsCheck(Type.Literal(true), Type.Object({}, { additionalProperties: false })) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Object 2 (Boolean)', () => { + type T = true extends { a: 10 } ? 1 : 2 + const R = ExtendsCheck(Type.Literal(true), Type.Object({ a: Type.Literal(10) }, { additionalProperties: true })) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Union 1 (Boolean)', () => { + type T = true extends number | string ? 1 : 2 + const R = ExtendsCheck(Type.Literal(true), Type.Union([Type.Number(), Type.String()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Union 2 (Boolean)', () => { + type T = true extends any | number ? 1 : 2 + const R = ExtendsCheck(Type.Literal(true), Type.Union([Type.Any(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 3 (Boolean)', () => { + type T = true extends boolean | number ? 1 : 2 + const R = ExtendsCheck(Type.Literal(true), Type.Union([Type.Boolean(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Null (Boolean)', () => { + type T = true extends null ? 1 : 2 + const R = ExtendsCheck(Type.Literal(true), Type.Null()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Undefined (Boolean)', () => { + type T = true extends undefined ? 1 : 2 + const R = ExtendsCheck(Type.Literal(true), Type.Undefined()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Void', () => { + type T = true extends void ? 1 : 2 + const R = ExtendsCheck(Type.Literal(true), Type.Void()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Date', () => { + type T = true extends Date ? 1 : 2 + const R = ExtendsCheck(Type.Literal(true), Type.Date()) + Assert.IsEqual(R, ExtendsResult.False) + }) +}) diff --git a/test/runtime/type/extends/not.ts b/test/runtime/type/extends/not.ts new file mode 100644 index 000000000..fd3a97ab8 --- /dev/null +++ b/test/runtime/type/extends/not.ts @@ -0,0 +1,144 @@ +import { Type, ExtendsCheck, ExtendsResult } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +// --------------------------------------------------------------------------- +// Note: Not is equivalent to Unknown with the exception of nested negation. +// --------------------------------------------------------------------------- +describe('type/extends/Not', () => { + // ------------------------------------------------------------------------- + // Issue: type T = number extends not number ? true : false // true + // type T = number extends unknown ? true : false // true + // + // TypeScript does not support type negation. The best TypeBox can do is + // treat "not" as "unknown". From this standpoint, the extends assignability + // check needs to return true for the following case to keep TypeBox aligned + // with TypeScript static inference. + // ------------------------------------------------------------------------- + it('Should extend with unknown assignability check', () => { + const A = Type.Number() + const B = Type.Not(Type.Number()) + const R = ExtendsCheck(A, B) + Assert.IsEqual(R, ExtendsResult.True) // we would expect false + }) + // --------------------------------------------------------------------------- + // Nested + // --------------------------------------------------------------------------- + it('Should extend with nested negation', () => { + const T1 = Type.String() + const T2 = Type.Not(T1) + const T3 = Type.Not(T2) + const T4 = Type.Not(T3) + const T5 = Type.Not(T4) + + const R1 = ExtendsCheck(T1, Type.String()) + const R2 = ExtendsCheck(T2, Type.String()) + const R3 = ExtendsCheck(T3, Type.String()) + const R4 = ExtendsCheck(T4, Type.String()) + const R5 = ExtendsCheck(T5, Type.String()) + + Assert.IsEqual(R1, ExtendsResult.True) + Assert.IsEqual(R2, ExtendsResult.False) + Assert.IsEqual(R3, ExtendsResult.True) + Assert.IsEqual(R4, ExtendsResult.False) + Assert.IsEqual(R5, ExtendsResult.True) + }) + + // --------------------------------------------------------------------------- + // Not as Unknown Tests + // --------------------------------------------------------------------------- + it('Should extend Any', () => { + type T = unknown extends any ? 1 : 2 + const R = ExtendsCheck(Type.Not(Type.Number()), Type.Any()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Unknown', () => { + type T = unknown extends unknown ? 1 : 2 + const R = ExtendsCheck(Type.Not(Type.Number()), Type.Unknown()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend String', () => { + type T = unknown extends string ? 1 : 2 + const R = ExtendsCheck(Type.Not(Type.Number()), Type.String()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Boolean', () => { + type T = unknown extends boolean ? 1 : 2 + const R = ExtendsCheck(Type.Not(Type.Number()), Type.Boolean()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Number', () => { + type T = unknown extends number ? 1 : 2 + const R = ExtendsCheck(Type.Not(Type.Number()), Type.Number()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Integer', () => { + type T = unknown extends number ? 1 : 2 + const R = ExtendsCheck(Type.Not(Type.Number()), Type.Integer()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Array 1', () => { + type T = unknown extends Array ? 1 : 2 + const R = ExtendsCheck(Type.Not(Type.Number()), Type.Array(Type.Any())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Array 2', () => { + type T = unknown extends Array ? 1 : 2 + const R = ExtendsCheck(Type.Not(Type.Number()), Type.Array(Type.String())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Tuple', () => { + type T = unknown extends [number, number] ? 1 : 2 + const R = ExtendsCheck(Type.Not(Type.Number()), Type.Tuple([Type.Number(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Object 1', () => { + type T = unknown extends {} ? 1 : 2 + const R = ExtendsCheck(Type.Not(Type.Number()), Type.Object({})) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Object 2', () => { + type T = unknown extends { a: number } ? 1 : 2 + const R = ExtendsCheck(Type.Not(Type.Number()), Type.Object({ a: Type.Number() })) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Union 1', () => { + type T = unknown extends number | string ? 1 : 2 + const R = ExtendsCheck(Type.Not(Type.Number()), Type.Union([Type.Number(), Type.String()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Union 2', () => { + type T = unknown extends any | number ? 1 : 2 // 1 + const R = ExtendsCheck(Type.Not(Type.Number()), Type.Union([Type.Any(), Type.String()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 3', () => { + type T = unknown extends unknown | number ? 1 : 2 // 1 + const R = ExtendsCheck(Type.Not(Type.Number()), Type.Union([Type.Unknown(), Type.String()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 4', () => { + type T = unknown extends unknown | any ? 1 : 2 // 1 + const R = ExtendsCheck(Type.Not(Type.Number()), Type.Union([Type.Unknown(), Type.String()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Null', () => { + type T = unknown extends null ? 1 : 2 + const R = ExtendsCheck(Type.Not(Type.Number()), Type.Null()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Undefined', () => { + type T = unknown extends undefined ? 1 : 2 + const R = ExtendsCheck(Type.Not(Type.Number()), Type.Undefined()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Void', () => { + type T = unknown extends void ? 1 : 2 + const R = ExtendsCheck(Type.Not(Type.Number()), Type.Void()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Date', () => { + type T = unknown extends Date ? 1 : 2 + const R = ExtendsCheck(Type.Not(Type.Number()), Type.Date()) + Assert.IsEqual(R, ExtendsResult.False) + }) +}) diff --git a/test/runtime/type/extends/null.ts b/test/runtime/type/extends/null.ts new file mode 100644 index 000000000..157d7d264 --- /dev/null +++ b/test/runtime/type/extends/null.ts @@ -0,0 +1,100 @@ +import { Type, ExtendsCheck, ExtendsResult } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('type/extends/Null', () => { + it('Should extend Any', () => { + type T = null extends any ? 1 : 2 + const R = ExtendsCheck(Type.Null(), Type.Any()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Unknown', () => { + type T = null extends unknown ? 1 : 2 + const R = ExtendsCheck(Type.Null(), Type.Unknown()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend String', () => { + type T = null extends string ? 1 : 2 + const R = ExtendsCheck(Type.Null(), Type.String()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Boolean', () => { + type T = null extends boolean ? 1 : 2 + const R = ExtendsCheck(Type.Null(), Type.Boolean()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Number', () => { + type T = null extends number ? 1 : 2 + const R = ExtendsCheck(Type.Null(), Type.Number()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Integer', () => { + type T = null extends number ? 1 : 2 + const R = ExtendsCheck(Type.Null(), Type.Integer()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Array', () => { + type T = null extends Array ? 1 : 2 + const R = ExtendsCheck(Type.Null(), Type.Array(Type.Any())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Tuple', () => { + type T = null extends [number, number] ? 1 : 2 + const R = ExtendsCheck(Type.Null(), Type.Tuple([Type.Number(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Record', () => { + type T = null extends Record ? 1 : 2 + const R = ExtendsCheck(Type.Null(), Type.Record(Type.Number(), Type.Any())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Object 1', () => { + type T = null extends {} ? 1 : 2 + const R = ExtendsCheck(Type.Null(), Type.Object({}, { additionalProperties: false })) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Object 2', () => { + type T = null extends { a: 10 } ? 1 : 2 + const R = ExtendsCheck(Type.Null(), Type.Object({ a: Type.Literal(10) }, { additionalProperties: true })) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Object 3', () => { + type T = null extends object ? 1 : 2 + const R = ExtendsCheck(Type.Null(), Type.Object({})) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Union 1', () => { + type T = null extends number | string ? 1 : 2 + const R = ExtendsCheck(Type.Null(), Type.Union([Type.Number(), Type.String()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Union 2', () => { + type T = null extends any | number ? 1 : 2 + const R = ExtendsCheck(Type.Null(), Type.Union([Type.Any(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 2', () => { + type T = null extends boolean | number ? 1 : 2 + const R = ExtendsCheck(Type.Null(), Type.Union([Type.Boolean(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Null', () => { + type T = null extends null ? 1 : 2 + const R = ExtendsCheck(Type.Null(), Type.Null()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Undefined', () => { + type T = null extends undefined ? 1 : 2 + const R = ExtendsCheck(Type.Null(), Type.Undefined()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Void', () => { + type T = null extends void ? 1 : 2 + const R = ExtendsCheck(Type.Null(), Type.Void()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Date', () => { + type T = null extends Date ? 1 : 2 + const R = ExtendsCheck(Type.Null(), Type.Date()) + Assert.IsEqual(R, ExtendsResult.False) + }) +}) diff --git a/test/runtime/type/extends/number.ts b/test/runtime/type/extends/number.ts new file mode 100644 index 000000000..ce9c4189d --- /dev/null +++ b/test/runtime/type/extends/number.ts @@ -0,0 +1,95 @@ +import { Type, ExtendsCheck, ExtendsResult } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('type/extends/Number', () => { + it('Should extend Any', () => { + type T = number extends any ? 1 : 2 + const R = ExtendsCheck(Type.Number(), Type.Any()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Unknown', () => { + type T = number extends unknown ? 1 : 2 + const R = ExtendsCheck(Type.Number(), Type.Unknown()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend String', () => { + type T = number extends string ? 1 : 2 + const R = ExtendsCheck(Type.Number(), Type.String()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Boolean', () => { + type T = number extends boolean ? 1 : 2 + const R = ExtendsCheck(Type.Number(), Type.Boolean()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Number', () => { + type T = number extends number ? 1 : 2 + const R = ExtendsCheck(Type.Number(), Type.Number()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Integer', () => { + type T = number extends number ? 1 : 2 + const R = ExtendsCheck(Type.Number(), Type.Integer()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Array', () => { + type T = number extends Array ? 1 : 2 + const R = ExtendsCheck(Type.Number(), Type.Array(Type.Any())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Tuple', () => { + type T = number extends [number, number] ? 1 : 2 + const R = ExtendsCheck(Type.Number(), Type.Tuple([Type.Number(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Record', () => { + type T = number extends Record ? 1 : 2 + const R = ExtendsCheck(Type.Number(), Type.Record(Type.Number(), Type.Any())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Object 1', () => { + type T = number extends {} ? 1 : 2 + const R = ExtendsCheck(Type.Number(), Type.Object({}, { additionalProperties: false })) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Object 2', () => { + type T = number extends { a: 10 } ? 1 : 2 + const R = ExtendsCheck(Type.Number(), Type.Object({ a: Type.Literal(10) }, { additionalProperties: true })) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Union 1', () => { + type T = number extends number | string ? 1 : 2 + const R = ExtendsCheck(Type.Number(), Type.Union([Type.Number(), Type.String()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 2', () => { + type T = number extends any | number ? 1 : 2 + const R = ExtendsCheck(Type.Number(), Type.Union([Type.Any(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 2', () => { + type T = number extends boolean | number ? 1 : 2 + const R = ExtendsCheck(Type.Number(), Type.Union([Type.Boolean(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Null', () => { + type T = number extends null ? 1 : 2 + const R = ExtendsCheck(Type.Number(), Type.Null()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Undefined', () => { + type T = number extends undefined ? 1 : 2 + const R = ExtendsCheck(Type.Number(), Type.Undefined()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Void', () => { + type T = number extends void ? 1 : 2 + const R = ExtendsCheck(Type.Number(), Type.Void()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Date', () => { + type T = number extends Date ? 1 : 2 + const R = ExtendsCheck(Type.Number(), Type.Date()) + Assert.IsEqual(R, ExtendsResult.False) + }) +}) diff --git a/test/runtime/type/extends/object.ts b/test/runtime/type/extends/object.ts new file mode 100644 index 000000000..76e405818 --- /dev/null +++ b/test/runtime/type/extends/object.ts @@ -0,0 +1,227 @@ +import { Type, ExtendsCheck, ExtendsResult } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('type/extends/Object', () => { + // ---------------------------------------------------------- + // Object + // ---------------------------------------------------------- + it('Should extend Object 1', () => { + type T = { x: number; y: number } extends { x: number } ? 1 : 2 + const A = Type.Object({ x: Type.Number(), y: Type.Number() }) + const B = Type.Object({ x: Type.Number() }) + const R = ExtendsCheck(A, B) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Object 2', () => { + type T = { x: number } extends { x: number; y: number } ? 1 : 2 + const A = Type.Object({ x: Type.Number() }) + const B = Type.Object({ x: Type.Number(), y: Type.Number() }) + const R = ExtendsCheck(A, B) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Object 3', () => { + type T = { x: number; y: string } extends { x: number } ? 1 : 2 + const A = Type.Object({ x: Type.Number(), y: Type.String() }) + const B = Type.Object({ x: Type.Number() }) + const R = ExtendsCheck(A, B) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Object 4', () => { + type T = { x: number } extends { x: number; y: string } ? 1 : 2 + const A = Type.Object({ x: Type.Number() }) + const B = Type.Object({ x: Type.Number(), y: Type.String() }) + const R = ExtendsCheck(A, B) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Object 5', () => { + type T = { x: number | string } extends { x: number } ? 1 : 2 + const A = Type.Object({ x: Type.Union([Type.Number(), Type.String()]) }) + const B = Type.Object({ x: Type.Number() }) + const R = ExtendsCheck(A, B) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Object 6', () => { + type T = { x: number } extends { x: number | string } ? 1 : 2 + const A = Type.Object({ x: Type.Number() }) + const B = Type.Object({ x: Type.Union([Type.Number(), Type.String()]) }) + const R = ExtendsCheck(A, B) + Assert.IsEqual(R, ExtendsResult.True) + }) + // ---------------------------------------------------------- + // Record + // ---------------------------------------------------------- + it('Should extend Record 2', () => { + type T = { a: number; b: number } extends Record ? 1 : 2 + const A = Type.Object({ a: Type.Number(), b: Type.Number() }) + const B = Type.Record(Type.String(), Type.Number()) + const R = ExtendsCheck(A, B) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Record 3', () => { + type T = { a: number; b: number } extends Record ? 1 : 2 + const A = Type.Object({ a: Type.Number(), b: Type.Number() }) + const B = Type.Record(Type.Number(), Type.Number()) + const R = ExtendsCheck(A, B) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Record 4', () => { + type T = { a: number; b: number } extends Record<'a' | 'b', number> ? 1 : 2 + const A = Type.Object({ a: Type.Number(), b: Type.Number() }) + const B = Type.Record(Type.Union([Type.Literal('a'), Type.Literal('b')]), Type.Number()) + const R = ExtendsCheck(A, B) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Record 5', () => { + type T = { a: number; b: number } extends Record<'a' | 'b', number> ? 1 : 2 + const A = Type.Object({ a: Type.Number(), b: Type.Number() }) + const B = Type.Record(Type.String(), Type.Number()) + const R = ExtendsCheck(A, B) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Record 6', () => { + type T = { a: number; b: number } extends Record<'a' | 'b', number> ? 1 : 2 + const A = Type.Object({ a: Type.Number(), b: Type.Number() }) + const B = Type.Record(Type.Number(), Type.Number()) + const R = ExtendsCheck(A, B) + Assert.IsEqual(R, ExtendsResult.True) + }) + // ---------------------------------------------------------- + // Standard + // ---------------------------------------------------------- + it('Should extend Any', () => { + type T = { a: number } extends any ? 1 : 2 + const R = ExtendsCheck(Type.Object({ a: Type.Number() }), Type.Any()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend String', () => { + type T = { a: number } extends string ? 1 : 2 + const R = ExtendsCheck(Type.Object({ a: Type.Number() }), Type.String()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Boolean', () => { + type T = { a: number } extends boolean ? 1 : 2 + const R = ExtendsCheck(Type.Object({ a: Type.Number() }), Type.Boolean()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Number', () => { + type T = { a: number } extends number ? 1 : 2 + const R = ExtendsCheck(Type.Object({ a: Type.Number() }), Type.Number()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Integer', () => { + type T = { a: number } extends number ? 1 : 2 + const R = ExtendsCheck(Type.Object({ a: Type.Number() }), Type.Integer()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Array', () => { + type T = { a: number } extends Array ? 1 : 2 + const R = ExtendsCheck(Type.Object({ a: Type.Number() }), Type.Array(Type.Any())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Tuple', () => { + type T = { a: number } extends [number, number] ? 1 : 2 + const R = ExtendsCheck(Type.Object({ a: Type.Number() }), Type.Tuple([Type.Number(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Object 1', () => { + type T = { a: number } extends {} ? 1 : 2 + const R = ExtendsCheck(Type.Object({ a: Type.Number() }), Type.Object({}, { additionalProperties: false })) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Object 2', () => { + type T = { a: number } extends { a: 10 } ? 1 : 2 + const R = ExtendsCheck(Type.Object({ a: Type.Number() }), Type.Object({ a: Type.Literal(10) })) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Object 3', () => { + type T = { a: number } extends object ? 1 : 2 + const R = ExtendsCheck(Type.Object({ a: Type.Number() }), Type.Object({})) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 1', () => { + type T = { a: number } extends number | string ? 1 : 2 + const R = ExtendsCheck(Type.Object({ a: Type.Number() }), Type.Union([Type.Null(), Type.String()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Union 2', () => { + type T = { a: number } extends any | number ? 1 : 2 + const R = ExtendsCheck(Type.Object({ a: Type.Number() }), Type.Union([Type.Any(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 2', () => { + type T = { a: number } extends boolean | number ? 1 : 2 + const R = ExtendsCheck(Type.Object({ a: Type.Number() }), Type.Union([Type.Boolean(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Null', () => { + type T = { a: number } extends null ? 1 : 2 + const R = ExtendsCheck(Type.Object({ a: Type.Number() }), Type.Null()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Undefined', () => { + type T = { a: number } extends undefined ? 1 : 2 + const R = ExtendsCheck(Type.Object({ a: Type.Number() }), Type.Undefined()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Void', () => { + type T = { a: number } extends void ? 1 : 2 + const R = ExtendsCheck(Type.Object({ a: Type.Number() }), Type.Void()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Date', () => { + type T = { a: number } extends Date ? 1 : 2 + const R = ExtendsCheck(Type.Object({ a: Type.Number() }), Type.Date()) + Assert.IsEqual(R, ExtendsResult.False) + }) + // ---------------------------------------------------------------- + // Optional + // ---------------------------------------------------------------- + it('Should extend optional 1', () => { + const A = Type.Object({ a: Type.Number() }) + const B = Type.Object({ a: Type.Optional(Type.Number()) }) + const C = ExtendsCheck(A, B) + Assert.IsEqual(C, ExtendsResult.True) + }) + it('Should extend optional 2', () => { + const A = Type.Object({ a: Type.Number() }) + const B = Type.Object({ a: Type.Optional(Type.Number()) }) + const C = ExtendsCheck(B, A) + Assert.IsEqual(C, ExtendsResult.False) + }) + it('Should extend optional 3', () => { + const A = Type.Object({ + x: Type.Optional(Type.Number()), + y: Type.Number(), + }) + const B = Type.Object({ + y: Type.Number(), + z: Type.Number(), + }) + const R = ExtendsCheck(A, B) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend optional 4', () => { + const A = Type.Object({ + x: Type.Number(), + y: Type.Number(), + }) + const B = Type.Object({ + y: Type.Number(), + z: Type.Number(), + }) + const R = ExtendsCheck(A, B) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend optional 5', () => { + const A = Type.Object({ + x: Type.Number(), + y: Type.Number(), + }) + const B = Type.Object({ + y: Type.Number(), + z: Type.Optional(Type.Number()), + }) + const R = ExtendsCheck(A, B) + Assert.IsEqual(R, ExtendsResult.True) + }) +}) diff --git a/test/runtime/type/extends/promise.ts b/test/runtime/type/extends/promise.ts new file mode 100644 index 000000000..2313c8738 --- /dev/null +++ b/test/runtime/type/extends/promise.ts @@ -0,0 +1,209 @@ +import { Type, ExtendsCheck, ExtendsResult } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('type/extends/Promise', () => { + // ---------------------------------------------- + // Generic Varying + // ---------------------------------------------- + it('Should extend Promise Varying 1', () => { + type T = Promise extends Promise ? 1 : 2 + const R = ExtendsCheck(Type.Promise(Type.Any()), Type.Promise(Type.Any())) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Promise Varying 2', () => { + type T = Promise extends Promise ? 1 : 2 + const R = ExtendsCheck(Type.Promise(Type.String()), Type.Promise(Type.Any())) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Promise Varying 3', () => { + type T = Promise extends Promise ? 1 : 2 // 1 + const R = ExtendsCheck(Type.Promise(Type.Any()), Type.Promise(Type.String())) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Promise Varying 4', () => { + type T = Promise extends Promise ? 1 : 2 + const R = ExtendsCheck(Type.Promise(Type.Number()), Type.Promise(Type.String())) + Assert.IsEqual(R, ExtendsResult.False) + }) + // ---------------------------------------------- + // Any + // ---------------------------------------------- + it('Should extend Any', () => { + type T = Promise extends any ? 1 : 2 + const R = ExtendsCheck(Type.Promise(Type.Any()), Type.Any()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Unknown', () => { + type T = Promise extends unknown ? 1 : 2 + const R = ExtendsCheck(Type.Promise(Type.Any()), Type.Unknown()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend String', () => { + type T = Promise extends string ? 1 : 2 + const R = ExtendsCheck(Type.Promise(Type.Any()), Type.String()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Boolean', () => { + type T = Promise extends boolean ? 1 : 2 + const R = ExtendsCheck(Type.Promise(Type.Any()), Type.Boolean()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Number', () => { + type T = Promise extends number ? 1 : 2 + const R = ExtendsCheck(Type.Promise(Type.Any()), Type.Number()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Integer', () => { + type T = Promise extends number ? 1 : 2 + const R = ExtendsCheck(Type.Promise(Type.Any()), Type.Integer()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Array', () => { + type T = Promise extends Array ? 1 : 2 + const R = ExtendsCheck(Type.Promise(Type.Any()), Type.Array(Type.Any())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Tuple', () => { + type T = Promise extends [number, number] ? 1 : 2 + const R = ExtendsCheck(Type.Promise(Type.Any()), Type.Tuple([Type.Number(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Record', () => { + type T = Promise extends Record ? 1 : 2 + const R = ExtendsCheck(Type.Promise(Type.Any()), Type.Record(Type.Number(), Type.Any())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Object 1', () => { + type T = Promise extends {} ? 1 : 2 + const R = ExtendsCheck(Type.Promise(Type.Any()), Type.Object({}, { additionalProperties: false })) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Object 2', () => { + type T = Promise extends { a: 10 } ? 1 : 2 + const R = ExtendsCheck(Type.Promise(Type.Any()), Type.Object({ a: Type.Literal(10) }, { additionalProperties: true })) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Object 3', () => { + type T = Promise extends object ? 1 : 2 + const R = ExtendsCheck(Type.Promise(Type.Any()), Type.Object({})) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 1', () => { + type T = Promise extends number | string ? 1 : 2 + const R = ExtendsCheck(Type.Promise(Type.Any()), Type.Union([Type.Number(), Type.String()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Union 2', () => { + type T = Promise extends any | number ? 1 : 2 + const R = ExtendsCheck(Type.Promise(Type.Any()), Type.Union([Type.Any(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 2', () => { + type T = Promise extends boolean | number ? 1 : 2 + const R = ExtendsCheck(Type.Promise(Type.Any()), Type.Union([Type.Boolean(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Null', () => { + type T = Promise extends null ? 1 : 2 + const R = ExtendsCheck(Type.Promise(Type.Any()), Type.Null()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Undefined', () => { + type T = Promise extends undefined ? 1 : 2 + const R = ExtendsCheck(Type.Promise(Type.Any()), Type.Undefined()) + Assert.IsEqual(R, ExtendsResult.False) + }) + // ---------------------------------------------- + // Constrained + // ---------------------------------------------- + it('Should extend constrained Any', () => { + type T = Promise extends any ? 1 : 2 + const R = ExtendsCheck(Type.Promise(Type.Number()), Type.Any()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend constrained Unknown', () => { + type T = Promise extends unknown ? 1 : 2 + const R = ExtendsCheck(Type.Promise(Type.Number()), Type.Unknown()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend constrained String', () => { + type T = Promise extends string ? 1 : 2 + const R = ExtendsCheck(Type.Promise(Type.Number()), Type.String()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend constrained Boolean', () => { + type T = Promise extends boolean ? 1 : 2 + const R = ExtendsCheck(Type.Promise(Type.Number()), Type.Boolean()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend constrained Number', () => { + type T = Promise extends number ? 1 : 2 + const R = ExtendsCheck(Type.Promise(Type.Any()), Type.Number()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend constrained Integer', () => { + type T = Promise extends number ? 1 : 2 + const R = ExtendsCheck(Type.Promise(Type.Number()), Type.Integer()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend constrained Array', () => { + type T = Promise extends Array ? 1 : 2 + const R = ExtendsCheck(Type.Promise(Type.Number()), Type.Array(Type.Any())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend constrained Tuple', () => { + type T = Promise extends [number, number] ? 1 : 2 + const R = ExtendsCheck(Type.Promise(Type.Number()), Type.Tuple([Type.Number(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend constrained Object 1', () => { + type T = Promise extends {} ? 1 : 2 + const R = ExtendsCheck(Type.Promise(Type.Number()), Type.Object({}, { additionalProperties: false })) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend constrained Object 2', () => { + type T = Promise extends { a: 10 } ? 1 : 2 + const R = ExtendsCheck(Type.Promise(Type.Number()), Type.Object({ a: Type.Literal(10) }, { additionalProperties: true })) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend constrained Object 3', () => { + type T = Promise extends object ? 1 : 2 + const R = ExtendsCheck(Type.Promise(Type.Number()), Type.Object({})) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend constrained Union 1', () => { + type T = Promise extends number | string ? 1 : 2 + const R = ExtendsCheck(Type.Promise(Type.Number()), Type.Union([Type.Number(), Type.String()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend constrained Union 2', () => { + type T = Promise extends any | number ? 1 : 2 + const R = ExtendsCheck(Type.Promise(Type.Number()), Type.Union([Type.Any(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend constrained Union 2', () => { + type T = Promise extends boolean | number ? 1 : 2 + const R = ExtendsCheck(Type.Promise(Type.Number()), Type.Union([Type.Boolean(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend constrained Null', () => { + type T = Promise extends null ? 1 : 2 + const R = ExtendsCheck(Type.Promise(Type.Number()), Type.Null()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend constrained Undefined', () => { + type T = Promise extends undefined ? 1 : 2 + const R = ExtendsCheck(Type.Promise(Type.Number()), Type.Undefined()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Void', () => { + type T = Promise extends void ? 1 : 2 + const R = ExtendsCheck(Type.Promise(Type.Number()), Type.Void()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Date', () => { + type T = Promise extends Date ? 1 : 2 + const R = ExtendsCheck(Type.Promise(Type.Number()), Type.Date()) + Assert.IsEqual(R, ExtendsResult.False) + }) +}) diff --git a/test/runtime/type/extends/record.ts b/test/runtime/type/extends/record.ts new file mode 100644 index 000000000..905a96fcf --- /dev/null +++ b/test/runtime/type/extends/record.ts @@ -0,0 +1,199 @@ +import { Type, ExtendsCheck, ExtendsResult } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('type/extends/Record', () => { + it('Should extend Record 1', () => { + type T = Record<'a' | 'b', number> extends { a: number; b: number } ? 1 : 2 + const A = Type.Record(Type.Union([Type.Literal('a'), Type.Literal('b')]), Type.Number()) + const B = Type.Object({ a: Type.Number(), b: Type.Number() }) + const R = ExtendsCheck(A, B) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Record 2', () => { + type T = Record extends { a: number; b: number } ? 1 : 2 + const A = Type.Record(Type.String(), Type.Number()) + const B = Type.Object({ a: Type.Number(), b: Type.Number() }) + const R = ExtendsCheck(A, B) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Record 3', () => { + type T = Record extends { a: number; b: number } ? 1 : 2 + const A = Type.Record(Type.Number(), Type.Number()) + const B = Type.Object({ a: Type.Number(), b: Type.Number() }) + const R = ExtendsCheck(A, B) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Record 4', () => { + type T = Record<'a' | 'b', number> extends { a: number; b: number } ? 1 : 2 + const A = Type.Record(Type.Union([Type.Literal('a'), Type.Literal('b')]), Type.Number()) + const B = Type.Object({ a: Type.Number(), b: Type.Number() }) + const R = ExtendsCheck(A, B) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Record 5', () => { + type T = Record<'a' | 'b', number> extends { a: number; b: number } ? 1 : 2 + const A = Type.Record(Type.Union([Type.Literal('a'), Type.Literal('b')]), Type.Number()) + const B = Type.Object({ a: Type.Number(), b: Type.Number() }) + const R = ExtendsCheck(A, B) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Record 6', () => { + type T = Record extends Record<'a' | 'b', number> ? true : false + const A = Type.Record(Type.String(), Type.Number()) + const B = Type.Record(Type.Union([Type.Literal('a'), Type.Literal('b')]), Type.Number()) + const R = ExtendsCheck(A, B) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Record 7', () => { + type T = Record extends Record ? 1 : 2 + const A = Type.Record(Type.String(), Type.Number()) + const B = Type.Record(Type.String(), Type.Number()) + const R = ExtendsCheck(A, B) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Record 8', () => { + type T = Record extends Record ? 1 : 2 + const A = Type.Record(Type.String(), Type.Number()) + const B = Type.Record(Type.Number(), Type.Number()) + const R = ExtendsCheck(A, B) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Record 9', () => { + type T = Record extends Record<'a' | 'b', number> ? 1 : 2 + const A = Type.Record(Type.Number(), Type.Number()) + const B = Type.Record(Type.Union([Type.Literal('a'), Type.Literal('b')]), Type.Number()) + const R = ExtendsCheck(A, B) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Record 10', () => { + type T = Record extends Record ? 1 : 2 + const A = Type.Record(Type.Number(), Type.Number()) + const B = Type.Record(Type.String(), Type.Number()) + const R = ExtendsCheck(A, B) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Record 11', () => { + type T = Record extends Record ? 1 : 2 + const A = Type.Record(Type.Number(), Type.Number()) + const B = Type.Record(Type.Number(), Type.Number()) + const R = ExtendsCheck(A, B) + Assert.IsEqual(R, ExtendsResult.True) + }) + // ----- + it('Should extend Record 12', () => { + type T = Record extends Record ? 1 : 2 + const A = Type.Record(Type.String(), Type.Number()) + const B = Type.Record(Type.Integer(), Type.Number()) + const R = ExtendsCheck(A, B) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Record 13', () => { + type T = Record extends Record<'a' | 'b', number> ? 1 : 2 + const A = Type.Record(Type.Integer(), Type.Number()) + const B = Type.Record(Type.Union([Type.Literal('a'), Type.Literal('b')]), Type.Number()) + const R = ExtendsCheck(A, B) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Record 14', () => { + type T = Record extends Record ? 1 : 2 + const A = Type.Record(Type.Integer(), Type.Number()) + const B = Type.Record(Type.String(), Type.Number()) + const R = ExtendsCheck(A, B) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Record 15', () => { + type T = Record extends Record ? 1 : 2 + const A = Type.Record(Type.Integer(), Type.Number()) + const B = Type.Record(Type.Integer(), Type.Number()) + const R = ExtendsCheck(A, B) + Assert.IsEqual(R, ExtendsResult.True) + }) + // ------------------------------------------------------------------- + // Standard + // ------------------------------------------------------------------- + it('Should extend Any', () => { + type T = Record extends any ? 1 : 2 + const R = ExtendsCheck(Type.Record(Type.Number(), Type.Number()), Type.Any()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Unknown', () => { + type T = Record extends unknown ? 1 : 2 + const R = ExtendsCheck(Type.Record(Type.Number(), Type.Number()), Type.Unknown()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend String', () => { + type T = Record extends string ? 1 : 2 + const R = ExtendsCheck(Type.Record(Type.Number(), Type.Number()), Type.String()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Boolean', () => { + type T = Record extends boolean ? 1 : 2 + const R = ExtendsCheck(Type.Record(Type.Number(), Type.Number()), Type.Boolean()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Number', () => { + type T = Record extends number ? 1 : 2 + const R = ExtendsCheck(Type.Record(Type.Number(), Type.Number()), Type.Number()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Integer', () => { + type T = Record extends number ? 1 : 2 + const R = ExtendsCheck(Type.Record(Type.Number(), Type.Number()), Type.Integer()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Array 1', () => { + type T = Record extends Array ? 1 : 2 + const R = ExtendsCheck(Type.Record(Type.Number(), Type.Number()), Type.Array(Type.Any())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Array 2', () => { + type T = Record extends Array ? 1 : 2 + const R = ExtendsCheck(Type.Record(Type.Number(), Type.Number()), Type.Array(Type.String())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Tuple', () => { + type T = Record extends [number, number] ? 1 : 2 + const R = ExtendsCheck(Type.Record(Type.Number(), Type.Number()), Type.Tuple([Type.Number(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Object 2', () => { + type T = Record extends {} ? 1 : 2 + const R = ExtendsCheck(Type.Record(Type.Number(), Type.Number()), Type.Object({})) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Object 3', () => { + type T = Record extends { a: number } ? 1 : 2 + const R = ExtendsCheck(Type.Record(Type.Number(), Type.Number()), Type.Object({ a: Type.Number() })) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Union 1', () => { + type T = Record extends number | string ? 1 : 2 + const R = ExtendsCheck(Type.Record(Type.Number(), Type.Number()), Type.Union([Type.Number(), Type.String()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Union 2', () => { + type T = Record extends any | number ? 1 : 2 + const R = ExtendsCheck(Type.Record(Type.Number(), Type.Number()), Type.Union([Type.Any(), Type.String()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Null', () => { + type T = Record extends null ? 1 : 2 + const R = ExtendsCheck(Type.Record(Type.Number(), Type.Number()), Type.Null()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Undefined', () => { + type T = Record extends undefined ? 1 : 2 + const R = ExtendsCheck(Type.Record(Type.Number(), Type.Number()), Type.Undefined()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Void', () => { + type T = Record extends void ? 1 : 2 + const R = ExtendsCheck(Type.Record(Type.Number(), Type.Number()), Type.Void()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Date', () => { + type T = Record extends Date ? 1 : 2 + const R = ExtendsCheck(Type.Record(Type.Number(), Type.Number()), Type.Date()) + Assert.IsEqual(R, ExtendsResult.False) + }) +}) diff --git a/test/runtime/type/extends/regexp.ts b/test/runtime/type/extends/regexp.ts new file mode 100644 index 000000000..2298acd23 --- /dev/null +++ b/test/runtime/type/extends/regexp.ts @@ -0,0 +1,113 @@ +import { Type, ExtendsCheck, ExtendsResult } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +// ------------------------------------------------------------------ +// Note: RegExp infers as type String +// ------------------------------------------------------------------ +describe('type/extends/RegExp', () => { + it('Should extend Any', () => { + type T = string extends any ? 1 : 2 + const R = ExtendsCheck(Type.RegExp(/xyz/), Type.Any()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Unknown', () => { + type T = string extends unknown ? 1 : 2 + const R = ExtendsCheck(Type.RegExp(/xyz/), Type.Unknown()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend String', () => { + type T = string extends string ? 1 : 2 + const R = ExtendsCheck(Type.RegExp(/xyz/), Type.RegExp(/xyz/)) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Boolean', () => { + type T = string extends boolean ? 1 : 2 + const R = ExtendsCheck(Type.RegExp(/xyz/), Type.Boolean()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Number', () => { + type T = string extends number ? 1 : 2 + const R = ExtendsCheck(Type.RegExp(/xyz/), Type.Number()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Integer', () => { + type T = string extends number ? 1 : 2 + const R = ExtendsCheck(Type.RegExp(/xyz/), Type.Integer()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Array', () => { + type T = string extends Array ? 1 : 2 + const R = ExtendsCheck(Type.RegExp(/xyz/), Type.Array(Type.Any())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Tuple', () => { + type T = string extends [number, number] ? 1 : 2 + const R = ExtendsCheck(Type.RegExp(/xyz/), Type.Tuple([Type.Number(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Record 1', () => { + type T = string extends Record ? 1 : 2 + const R = ExtendsCheck(Type.RegExp(/xyz/), Type.Record(Type.Number(), Type.Any())) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Record 2', () => { + type T = string extends Record ? 1 : 2 + const R = ExtendsCheck(Type.RegExp(/xyz/), Type.Record(Type.Number(), Type.Unknown())) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Record 3', () => { + type T = string extends Record ? 1 : 2 + const R = ExtendsCheck(Type.RegExp(/xyz/), Type.Record(Type.Number(), Type.RegExp(/xyz/))) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Record 4', () => { + type T = string extends Record ? 1 : 2 + const R = ExtendsCheck(Type.RegExp(/xyz/), Type.Record(Type.Number(), Type.Number())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Object 1', () => { + type T = string extends {} ? 1 : 2 + const R = ExtendsCheck(Type.RegExp(/xyz/), Type.Object({}, { additionalProperties: false })) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Object 2', () => { + type T = string extends { a: 10 } ? 1 : 2 + const R = ExtendsCheck(Type.RegExp(/xyz/), Type.Object({ a: Type.Literal(10) }, { additionalProperties: true })) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Union 1', () => { + type T = number extends number | string ? 1 : 2 + const R = ExtendsCheck(Type.Number(), Type.Union([Type.Number(), Type.RegExp(/xyz/)])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 2', () => { + type T = number extends any | number ? 1 : 2 + const R = ExtendsCheck(Type.Number(), Type.Union([Type.Any(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 3', () => { + type T = number extends boolean | number ? 1 : 2 + const R = ExtendsCheck(Type.Number(), Type.Union([Type.Boolean(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Null', () => { + type T = number extends null ? 1 : 2 + const R = ExtendsCheck(Type.Number(), Type.Null()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Undefined', () => { + type T = number extends undefined ? 1 : 2 + const R = ExtendsCheck(Type.Number(), Type.Undefined()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Void', () => { + type T = number extends void ? 1 : 2 + const R = ExtendsCheck(Type.Number(), Type.Void()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Date', () => { + type T = number extends Date ? 1 : 2 + const R = ExtendsCheck(Type.Number(), Type.Date()) + Assert.IsEqual(R, ExtendsResult.False) + }) +}) diff --git a/test/runtime/type/extends/string.ts b/test/runtime/type/extends/string.ts new file mode 100644 index 000000000..cdb3e216c --- /dev/null +++ b/test/runtime/type/extends/string.ts @@ -0,0 +1,110 @@ +import { Type, ExtendsCheck, ExtendsResult } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('type/extends/String', () => { + it('Should extend Any', () => { + type T = string extends any ? 1 : 2 + const R = ExtendsCheck(Type.String(), Type.Any()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Unknown', () => { + type T = string extends unknown ? 1 : 2 + const R = ExtendsCheck(Type.String(), Type.Unknown()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend String', () => { + type T = string extends string ? 1 : 2 + const R = ExtendsCheck(Type.String(), Type.String()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Boolean', () => { + type T = string extends boolean ? 1 : 2 + const R = ExtendsCheck(Type.String(), Type.Boolean()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Number', () => { + type T = string extends number ? 1 : 2 + const R = ExtendsCheck(Type.String(), Type.Number()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Integer', () => { + type T = string extends number ? 1 : 2 + const R = ExtendsCheck(Type.String(), Type.Integer()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Array', () => { + type T = string extends Array ? 1 : 2 + const R = ExtendsCheck(Type.String(), Type.Array(Type.Any())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Tuple', () => { + type T = string extends [number, number] ? 1 : 2 + const R = ExtendsCheck(Type.String(), Type.Tuple([Type.Number(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Record 1', () => { + type T = string extends Record ? 1 : 2 + const R = ExtendsCheck(Type.String(), Type.Record(Type.Number(), Type.Any())) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Record 2', () => { + type T = string extends Record ? 1 : 2 + const R = ExtendsCheck(Type.String(), Type.Record(Type.Number(), Type.Unknown())) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Record 3', () => { + type T = string extends Record ? 1 : 2 + const R = ExtendsCheck(Type.String(), Type.Record(Type.Number(), Type.String())) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Record 4', () => { + type T = string extends Record ? 1 : 2 + const R = ExtendsCheck(Type.String(), Type.Record(Type.Number(), Type.Number())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Object 1', () => { + type T = string extends {} ? 1 : 2 + const R = ExtendsCheck(Type.String(), Type.Object({}, { additionalProperties: false })) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Object 2', () => { + type T = string extends { a: 10 } ? 1 : 2 + const R = ExtendsCheck(Type.String(), Type.Object({ a: Type.Literal(10) }, { additionalProperties: true })) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Union 1', () => { + type T = number extends number | string ? 1 : 2 + const R = ExtendsCheck(Type.Number(), Type.Union([Type.Number(), Type.String()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 2', () => { + type T = number extends any | number ? 1 : 2 + const R = ExtendsCheck(Type.Number(), Type.Union([Type.Any(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 3', () => { + type T = number extends boolean | number ? 1 : 2 + const R = ExtendsCheck(Type.Number(), Type.Union([Type.Boolean(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Null', () => { + type T = number extends null ? 1 : 2 + const R = ExtendsCheck(Type.Number(), Type.Null()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Undefined', () => { + type T = number extends undefined ? 1 : 2 + const R = ExtendsCheck(Type.Number(), Type.Undefined()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Void', () => { + type T = number extends void ? 1 : 2 + const R = ExtendsCheck(Type.Number(), Type.Void()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Date', () => { + type T = number extends Date ? 1 : 2 + const R = ExtendsCheck(Type.Number(), Type.Date()) + Assert.IsEqual(R, ExtendsResult.False) + }) +}) diff --git a/test/runtime/type/extends/symbol.ts b/test/runtime/type/extends/symbol.ts new file mode 100644 index 000000000..9412f4fc8 --- /dev/null +++ b/test/runtime/type/extends/symbol.ts @@ -0,0 +1,105 @@ +import { Type, ExtendsCheck, ExtendsResult } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('type/extends/Symbol', () => { + it('Should extend Any', () => { + type T = symbol extends any ? 1 : 2 + const R = ExtendsCheck(Type.Symbol(), Type.Any()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Unknown', () => { + type T = symbol extends unknown ? 1 : 2 + const R = ExtendsCheck(Type.Symbol(), Type.Unknown()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend String', () => { + type T = symbol extends string ? 1 : 2 + const R = ExtendsCheck(Type.Symbol(), Type.String()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Boolean', () => { + type T = symbol extends boolean ? 1 : 2 + const R = ExtendsCheck(Type.Symbol(), Type.Boolean()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Number', () => { + type T = symbol extends number ? 1 : 2 + const R = ExtendsCheck(Type.Symbol(), Type.Number()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Integer', () => { + type T = symbol extends number ? 1 : 2 + const R = ExtendsCheck(Type.Symbol(), Type.Integer()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Array', () => { + type T = symbol extends Array ? 1 : 2 + const R = ExtendsCheck(Type.Symbol(), Type.Array(Type.Any())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Tuple', () => { + type T = symbol extends [number, number] ? 1 : 2 + const R = ExtendsCheck(Type.Symbol(), Type.Tuple([Type.Number(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Record', () => { + type T = symbol extends Record ? 1 : 2 + const R = ExtendsCheck(Type.Symbol(), Type.Record(Type.Number(), Type.Any())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Object 1', () => { + type T = symbol extends {} ? 1 : 2 + const R = ExtendsCheck(Type.Symbol(), Type.Object({})) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Object 2', () => { + type T = symbol extends { a: 10 } ? 1 : 2 + const R = ExtendsCheck(Type.Symbol(), Type.Object({ a: Type.Literal(10) })) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Union 1', () => { + type T = symbol extends number | string ? 1 : 2 + const R = ExtendsCheck(Type.Symbol(), Type.Union([Type.Number(), Type.String()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Union 2', () => { + type T = symbol extends any | number ? 1 : 2 + const R = ExtendsCheck(Type.Symbol(), Type.Union([Type.Any(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 3', () => { + type T = symbol extends boolean | number ? 1 : 2 + const R = ExtendsCheck(Type.Symbol(), Type.Union([Type.Boolean(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Union 4', () => { + type T = symbol extends boolean | symbol ? 1 : 2 + const R = ExtendsCheck(Type.Symbol(), Type.Union([Type.Boolean(), Type.Symbol()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Null', () => { + type T = symbol extends null ? 1 : 2 + const R = ExtendsCheck(Type.Symbol(), Type.Null()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Undefined', () => { + type T = symbol extends undefined ? 1 : 2 + const R = ExtendsCheck(Type.Symbol(), Type.Undefined()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Void', () => { + type T = symbol extends undefined ? 1 : 2 + const R = ExtendsCheck(Type.Symbol(), Type.Void()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Date', () => { + type T = symbol extends Date ? 1 : 2 + const R = ExtendsCheck(Type.Symbol(), Type.Date()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Symbol', () => { + type T = symbol extends symbol ? 1 : 2 + const R = ExtendsCheck(Type.Symbol(), Type.Symbol()) + Assert.IsEqual(R, ExtendsResult.True) + }) +}) diff --git a/test/runtime/type/extends/template-literal.ts b/test/runtime/type/extends/template-literal.ts new file mode 100644 index 000000000..1e8aeab12 --- /dev/null +++ b/test/runtime/type/extends/template-literal.ts @@ -0,0 +1,161 @@ +import { Type, ExtendsCheck, ExtendsResult } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('type/extends/TemplateLiteral', () => { + // ------------------------------------------------------------------- + // String Literal 'hello' + // ------------------------------------------------------------------- + it('Should extend Any (hello)', () => { + type T = 'hello' extends any ? 1 : 2 + const R = ExtendsCheck(Type.TemplateLiteral([Type.Literal('hello')]), Type.Any()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Unknown (hello)', () => { + type T = 'hello' extends unknown ? 1 : 2 + const R = ExtendsCheck(Type.TemplateLiteral([Type.Literal('hello')]), Type.Unknown()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend String (hello)', () => { + type T = 'hello' extends string ? 1 : 2 + const R = ExtendsCheck(Type.TemplateLiteral([Type.Literal('hello')]), Type.String()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Boolean (hello)', () => { + type T = 'hello' extends boolean ? 1 : 2 + const R = ExtendsCheck(Type.TemplateLiteral([Type.Literal('hello')]), Type.Boolean()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Number (hello)', () => { + type T = 'hello' extends number ? 1 : 2 + const R = ExtendsCheck(Type.TemplateLiteral([Type.Literal('hello')]), Type.Number()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Integer (hello)', () => { + type T = 'hello' extends number ? 1 : 2 + const R = ExtendsCheck(Type.TemplateLiteral([Type.Literal('hello')]), Type.Integer()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Array (hello)', () => { + type T = 'hello' extends Array ? 1 : 2 + const R = ExtendsCheck(Type.TemplateLiteral([Type.Literal('hello')]), Type.Array(Type.Any())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Tuple (hello)', () => { + type T = 'hello' extends [number, number] ? 1 : 2 + const R = ExtendsCheck(Type.TemplateLiteral([Type.Literal('hello')]), Type.Tuple([Type.Number(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Object 1 (hello)', () => { + type T = 'hello' extends {} ? 1 : 2 + const R = ExtendsCheck(Type.TemplateLiteral([Type.Literal('hello')]), Type.Object({}, { additionalProperties: false })) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Object 2 (hello)', () => { + type T = 'hello' extends { a: 10 } ? 1 : 2 + const R = ExtendsCheck(Type.TemplateLiteral([Type.Literal('hello')]), Type.Object({ a: Type.Literal(10) }, { additionalProperties: true })) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Union 1 (hello)', () => { + type T = 'hello' extends number | string ? 1 : 2 + const R = ExtendsCheck(Type.TemplateLiteral([Type.Literal('hello')]), Type.Union([Type.Number(), Type.String()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 2 (hello)', () => { + type T = 'hello' extends any | number ? 1 : 2 + const R = ExtendsCheck(Type.TemplateLiteral([Type.Literal('hello')]), Type.Union([Type.Any(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 3 (hello)', () => { + type T = 'hello' extends boolean | number ? 1 : 2 + const R = ExtendsCheck(Type.TemplateLiteral([Type.Literal('hello')]), Type.Union([Type.Boolean(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Null (hello)', () => { + type T = 'hello' extends null ? 1 : 2 + const R = ExtendsCheck(Type.TemplateLiteral([Type.Literal('hello')]), Type.Null()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Undefined (hello)', () => { + type T = 'hello' extends undefined ? 1 : 2 + const R = ExtendsCheck(Type.TemplateLiteral([Type.Literal('hello')]), Type.Undefined()) + Assert.IsEqual(R, ExtendsResult.False) + }) + // ------------------------------------------------------------------- + // String Literal 'hello' | 'world' + // ------------------------------------------------------------------- + it('Should extend Any (hello | world)', () => { + type T = 'hello' | 'world' extends any ? 1 : 2 + const R = ExtendsCheck(Type.TemplateLiteral([Type.Union([Type.Literal('hello'), Type.Literal('world')])]), Type.Any()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Unknown (hello | world)', () => { + type T = 'hello' | 'world' extends unknown ? 1 : 2 + const R = ExtendsCheck(Type.TemplateLiteral([Type.Union([Type.Literal('hello'), Type.Literal('world')])]), Type.Unknown()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend String (hello | world)', () => { + type T = 'hello' | 'world' extends string ? 1 : 2 + const R = ExtendsCheck(Type.TemplateLiteral([Type.Union([Type.Literal('hello'), Type.Literal('world')])]), Type.String()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Boolean (hello | world)', () => { + type T = 'hello' | 'world' extends boolean ? 1 : 2 + const R = ExtendsCheck(Type.TemplateLiteral([Type.Union([Type.Literal('hello'), Type.Literal('world')])]), Type.Boolean()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Number (hello | world)', () => { + type T = 'hello' | 'world' extends number ? 1 : 2 + const R = ExtendsCheck(Type.TemplateLiteral([Type.Union([Type.Literal('hello'), Type.Literal('world')])]), Type.Number()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Integer (hello | world)', () => { + type T = 'hello' | 'world' extends number ? 1 : 2 + const R = ExtendsCheck(Type.TemplateLiteral([Type.Union([Type.Literal('hello'), Type.Literal('world')])]), Type.Integer()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Array (hello | world)', () => { + type T = 'hello' | 'world' extends Array ? 1 : 2 + const R = ExtendsCheck(Type.TemplateLiteral([Type.Union([Type.Literal('hello'), Type.Literal('world')])]), Type.Array(Type.Any())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Tuple (hello | world)', () => { + type T = 'hello' | 'world' extends [number, number] ? 1 : 2 + const R = ExtendsCheck(Type.TemplateLiteral([Type.Union([Type.Literal('hello'), Type.Literal('world')])]), Type.Tuple([Type.Number(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Object 1 (hello | world)', () => { + type T = 'hello' | 'world' extends {} ? 1 : 2 + const R = ExtendsCheck(Type.TemplateLiteral([Type.Union([Type.Literal('hello'), Type.Literal('world')])]), Type.Object({}, { additionalProperties: false })) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Object 2 (hello | world)', () => { + type T = 'hello' | 'world' extends { a: 10 } ? 1 : 2 + const R = ExtendsCheck(Type.TemplateLiteral([Type.Union([Type.Literal('hello'), Type.Literal('world')])]), Type.Object({ a: Type.Literal(10) }, { additionalProperties: true })) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Union 1 (hello | world)', () => { + type T = 'hello' | 'world' extends number | string ? 1 : 2 + const R = ExtendsCheck(Type.TemplateLiteral([Type.Union([Type.Literal('hello'), Type.Literal('world')])]), Type.Union([Type.Number(), Type.String()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 2 (hello | world)', () => { + type T = 'hello' | 'world' extends any | number ? 1 : 2 + const R = ExtendsCheck(Type.TemplateLiteral([Type.Union([Type.Literal('hello'), Type.Literal('world')])]), Type.Union([Type.Any(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 3 (hello | world)', () => { + type T = 'hello' | 'world' extends boolean | number ? 1 : 2 + const R = ExtendsCheck(Type.TemplateLiteral([Type.Union([Type.Literal('hello'), Type.Literal('world')])]), Type.Union([Type.Boolean(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Null (hello | world)', () => { + type T = 'hello' | 'world' extends null ? 1 : 2 + const R = ExtendsCheck(Type.TemplateLiteral([Type.Union([Type.Literal('hello'), Type.Literal('world')])]), Type.Null()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Undefined (hello | world)', () => { + type T = 'hello' | 'world' extends undefined ? 1 : 2 + const R = ExtendsCheck(Type.TemplateLiteral([Type.Union([Type.Literal('hello'), Type.Literal('world')])]), Type.Undefined()) + Assert.IsEqual(R, ExtendsResult.False) + }) +}) diff --git a/test/runtime/type/extends/tuple.ts b/test/runtime/type/extends/tuple.ts new file mode 100644 index 000000000..99bc36033 --- /dev/null +++ b/test/runtime/type/extends/tuple.ts @@ -0,0 +1,160 @@ +import { Type, ExtendsCheck, ExtendsResult } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('type/extends/Tuple', () => { + it('Should extend Any', () => { + type T = [string, number] extends any ? 1 : 2 + const R = ExtendsCheck(Type.Tuple([Type.String(), Type.Number()]), Type.Any()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend String', () => { + type T = [string, number] extends string ? 1 : 2 + const R = ExtendsCheck(Type.Tuple([Type.String(), Type.Number()]), Type.String()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Boolean', () => { + type T = [string, number] extends boolean ? 1 : 2 + const R = ExtendsCheck(Type.Tuple([Type.String(), Type.Number()]), Type.Boolean()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Number', () => { + type T = [string, number] extends number ? 1 : 2 + const R = ExtendsCheck(Type.Tuple([Type.String(), Type.Number()]), Type.Number()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Integer', () => { + type T = [string, number] extends number ? 1 : 2 + const R = ExtendsCheck(Type.Tuple([Type.String(), Type.Number()]), Type.Integer()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Array 1', () => { + type T = [string, number] extends Array ? 1 : 2 // 1 + const R = ExtendsCheck(Type.Tuple([Type.String(), Type.Number()]), Type.Array(Type.Any())) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Array 2', () => { + type T = [string, number] extends Array ? 1 : 2 // 2 + const R = ExtendsCheck(Type.Tuple([Type.String(), Type.Number()]), Type.Array(Type.String())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Array 3', () => { + type T = [string, number] extends Array ? 1 : 2 // 1 + const R = ExtendsCheck(Type.Tuple([Type.String(), Type.Number()]), Type.Array(Type.Union([Type.String(), Type.Number()]))) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Array 4', () => { + type T = [string, number] extends Array ? 1 : 2 // 1 + const R = ExtendsCheck(Type.Tuple([Type.String(), Type.Number()]), Type.Array(Type.Union([Type.String(), Type.Number(), Type.Boolean()]))) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Tuple 1', () => { + type T = [string, number] extends [string, number] ? 1 : 2 + const R = ExtendsCheck(Type.Tuple([Type.String(), Type.Number()]), Type.Tuple([Type.String(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Tuple 2', () => { + type T = [string, number] extends [number, number] ? 1 : 2 + const R = ExtendsCheck(Type.Tuple([Type.String(), Type.Number()]), Type.Tuple([Type.Number(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Tuple 3', () => { + type T = [string, any] extends [number, number] ? 1 : 2 + const R = ExtendsCheck(Type.Tuple([Type.String(), Type.Any()]), Type.Tuple([Type.Number(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Tuple 4', () => { + type T = [string, number] extends [string, any] ? 1 : 2 + const R = ExtendsCheck(Type.Tuple([Type.String(), Type.Number()]), Type.Tuple([Type.String(), Type.Any()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Tuple 5', () => { + type T = [string, unknown] extends [number, number] ? 1 : 2 + const R = ExtendsCheck(Type.Tuple([Type.String(), Type.Unknown()]), Type.Tuple([Type.Number(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Tuple 6', () => { + type T = [string, number] extends [string, unknown] ? 1 : 2 + const R = ExtendsCheck(Type.Tuple([Type.String(), Type.Number()]), Type.Tuple([Type.String(), Type.Unknown()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Tuple 7', () => { + type T = [] extends [string, number] ? 1 : 2 + const R = ExtendsCheck(Type.Tuple([]), Type.Tuple([Type.String(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Tuple 8', () => { + type T = [string, number] extends [] ? 1 : 2 + const R = ExtendsCheck(Type.Tuple([Type.String(), Type.Number()]), Type.Tuple([])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Record 1', () => { + type T = [string, number] extends Record ? 1 : 2 + const R = ExtendsCheck(Type.String(), Type.Record(Type.Number(), Type.Any())) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Record 2', () => { + type T = [string, number] extends Record ? 1 : 2 + const R = ExtendsCheck(Type.String(), Type.Record(Type.Number(), Type.Unknown())) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Record 3', () => { + type T = [string, number] extends Record ? 1 : 2 + const R = ExtendsCheck(Type.String(), Type.Record(Type.Number(), Type.String())) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Record 4', () => { + type T = [string, number] extends Record ? 1 : 2 + const R = ExtendsCheck(Type.String(), Type.Record(Type.Number(), Type.Number())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Object 1', () => { + type T = [string, number] extends {} ? 1 : 2 + const R = ExtendsCheck(Type.Tuple([Type.String(), Type.Number()]), Type.Object({}, { additionalProperties: false })) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Object 2', () => { + type T = [string, number] extends { a: 10 } ? 1 : 2 + const R = ExtendsCheck(Type.Tuple([Type.String(), Type.Number()]), Type.Object({ a: Type.Literal(10) }, { additionalProperties: true })) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Object 3', () => { + type T = [string, number] extends object ? 1 : 2 + const R = ExtendsCheck(Type.Tuple([Type.String(), Type.Number()]), Type.Object({})) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 1', () => { + type T = [string, number] extends number | string ? 1 : 2 + const R = ExtendsCheck(Type.Tuple([Type.String(), Type.Number()]), Type.Union([Type.Number(), Type.String()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Union 2', () => { + type T = [string, number] extends any | number ? 1 : 2 + const R = ExtendsCheck(Type.Tuple([Type.String(), Type.Number()]), Type.Union([Type.Any(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 2', () => { + type T = [string, number] extends boolean | number ? 1 : 2 + const R = ExtendsCheck(Type.Tuple([Type.String(), Type.Number()]), Type.Union([Type.Boolean(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Null', () => { + type T = [string, number] extends null ? 1 : 2 + const R = ExtendsCheck(Type.Tuple([Type.String(), Type.Number()]), Type.Null()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Undefined', () => { + type T = [string, number] extends undefined ? 1 : 2 + const R = ExtendsCheck(Type.Tuple([Type.String(), Type.Number()]), Type.Undefined()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Void', () => { + type T = [string, number] extends void ? 1 : 2 + const R = ExtendsCheck(Type.Tuple([Type.String(), Type.Number()]), Type.Void()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Date', () => { + type T = [string, number] extends Date ? 1 : 2 + const R = ExtendsCheck(Type.Tuple([Type.String(), Type.Number()]), Type.Date()) + Assert.IsEqual(R, ExtendsResult.False) + }) +}) diff --git a/test/runtime/type/extends/uint8array.ts b/test/runtime/type/extends/uint8array.ts new file mode 100644 index 000000000..0128fb0cb --- /dev/null +++ b/test/runtime/type/extends/uint8array.ts @@ -0,0 +1,95 @@ +import { Type, ExtendsCheck, ExtendsResult } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('type/extends/Uint8Array', () => { + it('Should extend Any', () => { + type T = Uint8Array extends any ? 1 : 2 + const R = ExtendsCheck(Type.Uint8Array(), Type.Any()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Unknown', () => { + type T = Uint8Array extends unknown ? 1 : 2 + const R = ExtendsCheck(Type.Uint8Array(), Type.Unknown()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend String', () => { + type T = Uint8Array extends string ? 1 : 2 + const R = ExtendsCheck(Type.Uint8Array(), Type.String()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Boolean', () => { + type T = Uint8Array extends boolean ? 1 : 2 + const R = ExtendsCheck(Type.Uint8Array(), Type.Boolean()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Number', () => { + type T = Uint8Array extends number ? 1 : 2 + const R = ExtendsCheck(Type.Uint8Array(), Type.Number()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Integer', () => { + type T = Uint8Array extends number ? 1 : 2 + const R = ExtendsCheck(Type.Uint8Array(), Type.Integer()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Array', () => { + type T = Uint8Array extends Array ? 1 : 2 + const R = ExtendsCheck(Type.Uint8Array(), Type.Array(Type.Any())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Tuple', () => { + type T = Uint8Array extends [number, number] ? 1 : 2 + const R = ExtendsCheck(Type.Uint8Array(), Type.Tuple([Type.Number(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Record', () => { + type T = Uint8Array extends Record ? 1 : 2 + const R = ExtendsCheck(Type.Uint8Array(), Type.Record(Type.Number(), Type.Any())) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Object 1', () => { + type T = Uint8Array extends {} ? 1 : 2 + const R = ExtendsCheck(Type.Uint8Array(), Type.Object({}, { additionalProperties: false })) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Object 2', () => { + type T = Uint8Array extends { a: 10 } ? 1 : 2 + const R = ExtendsCheck(Type.Uint8Array(), Type.Object({ a: Type.Literal(10) }, { additionalProperties: true })) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Union 1', () => { + type T = Uint8Array extends number | string ? 1 : 2 + const R = ExtendsCheck(Type.Uint8Array(), Type.Union([Type.Number(), Type.String()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Union 2', () => { + type T = Uint8Array extends any | number ? 1 : 2 + const R = ExtendsCheck(Type.Uint8Array(), Type.Union([Type.Any(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 3', () => { + type T = Uint8Array extends boolean | number ? 1 : 2 + const R = ExtendsCheck(Type.Uint8Array(), Type.Union([Type.Boolean(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Null', () => { + type T = Uint8Array extends null ? 1 : 2 + const R = ExtendsCheck(Type.Uint8Array(), Type.Null()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Undefined', () => { + type T = Uint8Array extends undefined ? 1 : 2 + const R = ExtendsCheck(Type.Uint8Array(), Type.Undefined()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Void', () => { + type T = Uint8Array extends void ? 1 : 2 + const R = ExtendsCheck(Type.Uint8Array(), Type.Void()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Date', () => { + type T = Uint8Array extends Date ? 1 : 2 + const R = ExtendsCheck(Type.Uint8Array(), Type.Date()) + Assert.IsEqual(R, ExtendsResult.False) + }) +}) diff --git a/test/runtime/type/extends/undefined.ts b/test/runtime/type/extends/undefined.ts new file mode 100644 index 000000000..4996a1054 --- /dev/null +++ b/test/runtime/type/extends/undefined.ts @@ -0,0 +1,90 @@ +import { Type, ExtendsCheck, ExtendsResult } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('type/extends/Undefined', () => { + it('Should extend Any', () => { + type T = undefined extends any ? 1 : 2 + const R = ExtendsCheck(Type.Undefined(), Type.Any()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend String', () => { + type T = undefined extends string ? 1 : 2 + const R = ExtendsCheck(Type.Undefined(), Type.String()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Boolean', () => { + type T = undefined extends boolean ? 1 : 2 + const R = ExtendsCheck(Type.Undefined(), Type.Boolean()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Number', () => { + type T = undefined extends number ? 1 : 2 + const R = ExtendsCheck(Type.Undefined(), Type.Number()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Integer', () => { + type T = undefined extends number ? 1 : 2 + const R = ExtendsCheck(Type.Undefined(), Type.Integer()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Array', () => { + type T = undefined extends Array ? 1 : 2 + const R = ExtendsCheck(Type.Undefined(), Type.Array(Type.Any())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Tuple', () => { + type T = undefined extends [number, number] ? 1 : 2 + const R = ExtendsCheck(Type.Undefined(), Type.Tuple([Type.Number(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Object 1', () => { + type T = undefined extends {} ? 1 : 2 + const R = ExtendsCheck(Type.Undefined(), Type.Object({}, { additionalProperties: false })) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Object 2', () => { + type T = undefined extends { a: 10 } ? 1 : 2 + const R = ExtendsCheck(Type.Undefined(), Type.Object({ a: Type.Literal(10) }, { additionalProperties: true })) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Object 3', () => { + type T = undefined extends object ? 1 : 2 + const R = ExtendsCheck(Type.Undefined(), Type.Object({})) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Union 1', () => { + type T = undefined extends number | string ? 1 : 2 + const R = ExtendsCheck(Type.Undefined(), Type.Union([Type.Number(), Type.String()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Union 2', () => { + type T = undefined extends any | number ? 1 : 2 + const R = ExtendsCheck(Type.Undefined(), Type.Union([Type.Any(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 2', () => { + type T = undefined extends boolean | number ? 1 : 2 + const R = ExtendsCheck(Type.Undefined(), Type.Union([Type.Boolean(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Null', () => { + type T = undefined extends null ? 1 : 2 + const R = ExtendsCheck(Type.Undefined(), Type.Null()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Undefined', () => { + type T = undefined extends undefined ? 1 : 2 + const R = ExtendsCheck(Type.Undefined(), Type.Undefined()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Void', () => { + type T = undefined extends void ? 1 : 2 + const R = ExtendsCheck(Type.Undefined(), Type.Void()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Date', () => { + type T = undefined extends Date ? 1 : 2 + const R = ExtendsCheck(Type.Undefined(), Type.Date()) + Assert.IsEqual(R, ExtendsResult.False) + }) +}) diff --git a/test/runtime/type/extends/union.ts b/test/runtime/type/extends/union.ts new file mode 100644 index 000000000..39783cff9 --- /dev/null +++ b/test/runtime/type/extends/union.ts @@ -0,0 +1,135 @@ +import { Type, ExtendsCheck, ExtendsResult } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('type/extends/Union', () => { + it('Should extend Any', () => { + type T = number | string extends any ? 1 : 2 + const R = ExtendsCheck(Type.Union([Type.Number(), Type.String()]), Type.Any()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend String', () => { + type T = number | string extends string ? 1 : 2 + const R = ExtendsCheck(Type.Union([Type.Number(), Type.String()]), Type.String()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Boolean', () => { + type T = number | string extends boolean ? 1 : 2 + const R = ExtendsCheck(Type.Union([Type.Number(), Type.String()]), Type.Boolean()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Number', () => { + type T = number | string extends number ? 1 : 2 + const R = ExtendsCheck(Type.Union([Type.Number(), Type.String()]), Type.Number()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Integer', () => { + type T = number | string extends number ? 1 : 2 + const R = ExtendsCheck(Type.Union([Type.Number(), Type.String()]), Type.Integer()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Array', () => { + type T = number | string extends Array ? 1 : 2 + const R = ExtendsCheck(Type.Union([Type.Number(), Type.String()]), Type.Array(Type.Any())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Tuple', () => { + type T = number | string extends [number, number] ? 1 : 2 + const R = ExtendsCheck(Type.Union([Type.Number(), Type.String()]), Type.Tuple([Type.Number(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Object 1', () => { + type T = number | string extends {} ? 1 : 2 + const R = ExtendsCheck(Type.Union([Type.Number(), Type.String()]), Type.Object({}, { additionalProperties: false })) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Object 2', () => { + type T = number | string extends { a: 10 } ? 1 : 2 + const R = ExtendsCheck(Type.Union([Type.Number(), Type.String()]), Type.Object({ a: Type.Literal(10) }, { additionalProperties: true })) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Union 1', () => { + type T = number | string extends number | string ? 1 : 2 + const R = ExtendsCheck(Type.Union([Type.Number(), Type.String()]), Type.Union([Type.Number(), Type.String()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 2', () => { + type T = number | string extends any | number ? 1 : 2 + const R = ExtendsCheck(Type.Union([Type.Number(), Type.String()]), Type.Union([Type.Any(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 3', () => { + type T = number | string extends boolean | number ? 1 : 2 + const R = ExtendsCheck(Type.Union([Type.Number(), Type.String()]), Type.Union([Type.Boolean(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Union 4', () => { + type T = any | boolean extends boolean | number ? 1 : 2 + const R = ExtendsCheck(Type.Any(), Type.Union([Type.Boolean(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.Union) + }) + it('Should extend Union 5', () => { + type T = any | string extends boolean | number ? 1 : 2 + const R = ExtendsCheck(Type.Any(), Type.Union([Type.Boolean(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.Union) + }) + it('Should extend Union 6', () => { + type T = any | {} extends {} ? 1 : 2 + const R = ExtendsCheck(Type.Any(), Type.Union([Type.Boolean(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.Union) + }) + it('Should extend Union 7', () => { + type T = any extends boolean | number ? 1 : 2 + const R = ExtendsCheck(Type.Any(), Type.Union([Type.Boolean(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.Union) + }) + it('Should extend Union 8', () => { + type T = unknown | string extends boolean | number ? 1 : 2 + const R = ExtendsCheck(Type.Union([Type.Unknown(), Type.String()]), Type.Union([Type.Boolean(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Union 9', () => { + type T = unknown extends boolean | number ? 1 : 2 + const R = ExtendsCheck(Type.Unknown(), Type.Union([Type.Boolean(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Null', () => { + type T = number | string extends null ? 1 : 2 + const R = ExtendsCheck(Type.Union([Type.Number(), Type.String()]), Type.Null()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Undefined', () => { + type T = number | string extends undefined ? 1 : 2 + const R = ExtendsCheck(Type.Union([Type.Number(), Type.String()]), Type.Undefined()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Void', () => { + type T = number | string extends void ? 1 : 2 + const R = ExtendsCheck(Type.Union([Type.Number(), Type.String()]), Type.Void()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Void 2', () => { + type T = number | string | void extends void ? 1 : 2 + const R = ExtendsCheck(Type.Union([Type.Number(), Type.String()]), Type.Void()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Date', () => { + type T = number | string | void extends Date ? 1 : 2 + const R = ExtendsCheck(Type.Union([Type.Number(), Type.String()]), Type.Date()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Date 2', () => { + type T = Date | number | string | void extends Date ? 1 : 2 + const R = ExtendsCheck(Type.Union([Type.Date(), Type.Number(), Type.String()]), Type.Date()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend BigInt', () => { + type T = bigint | number | string | void extends Date ? 1 : 2 + const R = ExtendsCheck(Type.Union([Type.BigInt(), Type.Number(), Type.String()]), Type.Date()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Symbol', () => { + type T = symbol | number | string | void extends Date ? 1 : 2 + const R = ExtendsCheck(Type.Union([Type.Symbol(), Type.Number(), Type.String()]), Type.Date()) + Assert.IsEqual(R, ExtendsResult.False) + }) +}) diff --git a/test/runtime/type/extends/unknown.ts b/test/runtime/type/extends/unknown.ts new file mode 100644 index 000000000..7622dbfdd --- /dev/null +++ b/test/runtime/type/extends/unknown.ts @@ -0,0 +1,100 @@ +import { Type, ExtendsCheck, ExtendsResult } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('type/extends/Unknown', () => { + it('Should extend Any', () => { + type T = unknown extends any ? 1 : 2 + const R = ExtendsCheck(Type.Unknown(), Type.Any()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Unknown', () => { + type T = unknown extends unknown ? 1 : 2 + const R = ExtendsCheck(Type.Unknown(), Type.Unknown()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend String', () => { + type T = unknown extends string ? 1 : 2 + const R = ExtendsCheck(Type.Unknown(), Type.String()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Boolean', () => { + type T = unknown extends boolean ? 1 : 2 + const R = ExtendsCheck(Type.Unknown(), Type.Boolean()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Number', () => { + type T = unknown extends number ? 1 : 2 + const R = ExtendsCheck(Type.Unknown(), Type.Number()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Integer', () => { + type T = unknown extends number ? 1 : 2 + const R = ExtendsCheck(Type.Unknown(), Type.Integer()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Array 1', () => { + type T = unknown extends Array ? 1 : 2 + const R = ExtendsCheck(Type.Unknown(), Type.Array(Type.Any())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Array 2', () => { + type T = unknown extends Array ? 1 : 2 + const R = ExtendsCheck(Type.Unknown(), Type.Array(Type.String())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Tuple', () => { + type T = unknown extends [number, number] ? 1 : 2 + const R = ExtendsCheck(Type.Unknown(), Type.Tuple([Type.Number(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Object 1', () => { + type T = unknown extends {} ? 1 : 2 + const R = ExtendsCheck(Type.Unknown(), Type.Object({})) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Object 2', () => { + type T = unknown extends { a: number } ? 1 : 2 + const R = ExtendsCheck(Type.Unknown(), Type.Object({ a: Type.Number() })) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Union 1', () => { + type T = unknown extends number | string ? 1 : 2 + const R = ExtendsCheck(Type.Unknown(), Type.Union([Type.Number(), Type.String()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Union 2', () => { + type T = unknown extends any | number ? 1 : 2 // 1 + const R = ExtendsCheck(Type.Unknown(), Type.Union([Type.Any(), Type.String()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 3', () => { + type T = unknown extends unknown | number ? 1 : 2 // 1 + const R = ExtendsCheck(Type.Unknown(), Type.Union([Type.Unknown(), Type.String()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 4', () => { + type T = unknown extends unknown | any ? 1 : 2 // 1 + const R = ExtendsCheck(Type.Unknown(), Type.Union([Type.Unknown(), Type.String()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Null', () => { + type T = unknown extends null ? 1 : 2 + const R = ExtendsCheck(Type.Unknown(), Type.Null()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Undefined', () => { + type T = unknown extends undefined ? 1 : 2 + const R = ExtendsCheck(Type.Unknown(), Type.Undefined()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Void', () => { + type T = unknown extends void ? 1 : 2 + const R = ExtendsCheck(Type.Unknown(), Type.Void()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Date', () => { + type T = unknown extends Date ? 1 : 2 + const R = ExtendsCheck(Type.Unknown(), Type.Date()) + Assert.IsEqual(R, ExtendsResult.False) + }) +}) diff --git a/test/runtime/type/extends/void.ts b/test/runtime/type/extends/void.ts new file mode 100644 index 000000000..48efc644e --- /dev/null +++ b/test/runtime/type/extends/void.ts @@ -0,0 +1,105 @@ +import { Type, ExtendsCheck, ExtendsResult } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('type/extends/Void', () => { + it('Should extend Any', () => { + type T = void extends any ? 1 : 2 + const R = ExtendsCheck(Type.Void(), Type.Any()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Unknown', () => { + type T = void extends unknown ? 1 : 2 + const R = ExtendsCheck(Type.Void(), Type.Unknown()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend String', () => { + type T = void extends string ? 1 : 2 + const R = ExtendsCheck(Type.Void(), Type.String()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Boolean', () => { + type T = void extends boolean ? 1 : 2 + const R = ExtendsCheck(Type.Void(), Type.Boolean()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Number', () => { + type T = void extends number ? 1 : 2 + const R = ExtendsCheck(Type.Void(), Type.Number()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Integer', () => { + type T = void extends number ? 1 : 2 + const R = ExtendsCheck(Type.Void(), Type.Integer()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Array 1', () => { + type T = void extends Array ? 1 : 2 + const R = ExtendsCheck(Type.Void(), Type.Array(Type.Any())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Array 2', () => { + type T = void extends Array ? 1 : 2 + const R = ExtendsCheck(Type.Void(), Type.Array(Type.String())) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Tuple', () => { + type T = void extends [number, number] ? 1 : 2 + const R = ExtendsCheck(Type.Void(), Type.Tuple([Type.Number(), Type.Number()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Object 1', () => { + type T = void extends object ? 1 : 2 + const R = ExtendsCheck(Type.Void(), Type.Object({})) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Object 2', () => { + type T = void extends {} ? 1 : 2 + const R = ExtendsCheck(Type.Void(), Type.Object({})) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Object 3', () => { + type T = void extends { a: number } ? 1 : 2 + const R = ExtendsCheck(Type.Void(), Type.Object({ a: Type.Number() })) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Union 1', () => { + type T = void extends number | string ? 1 : 2 + const R = ExtendsCheck(Type.Void(), Type.Union([Type.Number(), Type.String()])) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Union 2', () => { + type T = void extends any | number ? 1 : 2 // 1 + const R = ExtendsCheck(Type.Void(), Type.Union([Type.Any(), Type.String()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 3', () => { + type T = void extends unknown | number ? 1 : 2 // 1 + const R = ExtendsCheck(Type.Void(), Type.Union([Type.Unknown(), Type.String()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Union 4', () => { + type T = void extends unknown | any ? 1 : 2 // 1 + const R = ExtendsCheck(Type.Void(), Type.Union([Type.Unknown(), Type.String()])) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Null', () => { + type T = void extends null ? 1 : 2 + const R = ExtendsCheck(Type.Void(), Type.Null()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Undefined', () => { + type T = void extends undefined ? 1 : 2 + const R = ExtendsCheck(Type.Void(), Type.Undefined()) + Assert.IsEqual(R, ExtendsResult.False) + }) + it('Should extend Void', () => { + type T = void extends void ? 1 : 2 + const R = ExtendsCheck(Type.Void(), Type.Void()) + Assert.IsEqual(R, ExtendsResult.True) + }) + it('Should extend Date', () => { + type T = void extends Date ? 1 : 2 + const R = ExtendsCheck(Type.Void(), Type.Date()) + Assert.IsEqual(R, ExtendsResult.False) + }) +}) diff --git a/test/runtime/type/guard/index.ts b/test/runtime/type/guard/index.ts new file mode 100644 index 000000000..2a6bdd114 --- /dev/null +++ b/test/runtime/type/guard/index.ts @@ -0,0 +1,3 @@ +import './type/index' +import './kind/index' +import './value/index' diff --git a/test/runtime/type/guard/kind/any.ts b/test/runtime/type/guard/kind/any.ts new file mode 100644 index 000000000..87a860a0a --- /dev/null +++ b/test/runtime/type/guard/kind/any.ts @@ -0,0 +1,14 @@ +import { KindGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TAny', () => { + it('Should guard for TAny', () => { + const R = KindGuard.IsAny(Type.Any()) + Assert.IsTrue(R) + }) + it('Should not guard for TAny', () => { + const R = KindGuard.IsAny(null) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/guard/kind/argument.ts b/test/runtime/type/guard/kind/argument.ts new file mode 100644 index 000000000..5739e1eee --- /dev/null +++ b/test/runtime/type/guard/kind/argument.ts @@ -0,0 +1,14 @@ +import { KindGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TArgument', () => { + it('Should guard for TArgument', () => { + const R = KindGuard.IsArgument(Type.Argument(0)) + Assert.IsTrue(R) + }) + it('Should not guard for TArgument', () => { + const R = KindGuard.IsArgument(null) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/guard/kind/array.ts b/test/runtime/type/guard/kind/array.ts new file mode 100644 index 000000000..6f7ce4454 --- /dev/null +++ b/test/runtime/type/guard/kind/array.ts @@ -0,0 +1,14 @@ +import { KindGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TArray', () => { + it('Should guard for TArray', () => { + const R = KindGuard.IsArray(Type.Array(Type.Number())) + Assert.IsTrue(R) + }) + it('Should not guard for TArray', () => { + const R = KindGuard.IsArray(null) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/guard/kind/async-iterator.ts b/test/runtime/type/guard/kind/async-iterator.ts new file mode 100644 index 000000000..3e6d16e8d --- /dev/null +++ b/test/runtime/type/guard/kind/async-iterator.ts @@ -0,0 +1,16 @@ +import { KindGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TAsyncIterator', () => { + it('Should guard for TAsyncIterator', () => { + const T = Type.AsyncIterator(Type.Any()) + const R = KindGuard.IsAsyncIterator(T) + Assert.IsTrue(R) + }) + it('Should not guard for TAsyncIterator', () => { + const T = null + const R = KindGuard.IsAsyncIterator(T) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/guard/kind/awaited.ts b/test/runtime/type/guard/kind/awaited.ts new file mode 100644 index 000000000..021c7f62d --- /dev/null +++ b/test/runtime/type/guard/kind/awaited.ts @@ -0,0 +1,41 @@ +import { KindGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/Awaited', () => { + it('Should guard for Awaited 1', () => { + const T = Type.Awaited(Type.String()) + const R = KindGuard.IsString(T) + Assert.IsTrue(R) + }) + it('Should guard for Awaited 2', () => { + const T = Type.Awaited(Type.Promise(Type.String())) + const R = KindGuard.IsString(T) + Assert.IsTrue(R) + }) + it('Should guard for Awaited 3', () => { + const T = Type.Awaited(Type.Awaited(Type.Promise(Type.String()))) + const R = KindGuard.IsString(T) + Assert.IsTrue(R) + }) + it('Should guard for Awaited 4', () => { + const T = Type.Awaited(Type.Union([Type.Promise(Type.Promise(Type.String()))])) + Assert.IsTrue(KindGuard.IsString(T)) + }) + it('Should guard for Awaited 5', () => { + const T = Type.Awaited(Type.Union([Type.Promise(Type.Promise(Type.String())), Type.Number()])) + Assert.IsTrue(KindGuard.IsUnion(T)) + Assert.IsTrue(KindGuard.IsString(T.anyOf[0])) + Assert.IsTrue(KindGuard.IsNumber(T.anyOf[1])) + }) + it('Should guard for Awaited 6', () => { + const T = Type.Awaited(Type.Intersect([Type.Promise(Type.Promise(Type.String()))])) + Assert.IsTrue(KindGuard.IsString(T)) + }) + it('Should guard for Awaited 7', () => { + const T = Type.Awaited(Type.Intersect([Type.Promise(Type.Promise(Type.String())), Type.Number()])) + Assert.IsTrue(KindGuard.IsIntersect(T)) + Assert.IsTrue(KindGuard.IsString(T.allOf[0])) + Assert.IsTrue(KindGuard.IsNumber(T.allOf[1])) + }) +}) diff --git a/test/runtime/type/guard/kind/bigint.ts b/test/runtime/type/guard/kind/bigint.ts new file mode 100644 index 000000000..33e89b2be --- /dev/null +++ b/test/runtime/type/guard/kind/bigint.ts @@ -0,0 +1,14 @@ +import { KindGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TBigInt', () => { + it('Should guard for TBigInt', () => { + const R = KindGuard.IsBigInt(Type.BigInt()) + Assert.IsTrue(R) + }) + it('Should not guard for TBigInt', () => { + const R = KindGuard.IsBigInt(null) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/guard/kind/boolean.ts b/test/runtime/type/guard/kind/boolean.ts new file mode 100644 index 000000000..abdfd262d --- /dev/null +++ b/test/runtime/type/guard/kind/boolean.ts @@ -0,0 +1,14 @@ +import { KindGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TBoolean', () => { + it('Should guard for TBoolean', () => { + const R = KindGuard.IsBoolean(Type.Boolean()) + Assert.IsTrue(R) + }) + it('Should not guard for TBoolean', () => { + const R = KindGuard.IsBoolean(null) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/guard/kind/capitalize.ts b/test/runtime/type/guard/kind/capitalize.ts new file mode 100644 index 000000000..373937702 --- /dev/null +++ b/test/runtime/type/guard/kind/capitalize.ts @@ -0,0 +1,33 @@ +import { KindGuard, Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/Capitalize', () => { + it('Should guard for Capitalize 1', () => { + const T = Type.Capitalize(Type.Literal('hello'), { $id: 'hello', foo: 1 }) + Assert.IsTrue(KindGuard.IsLiteral(T)) + Assert.IsEqual(T.const, 'Hello') + Assert.IsEqual(T.$id, 'hello') + Assert.IsEqual(T.foo, 1) + }) + it('Should guard for Capitalize 2', () => { + const T = Type.Capitalize(Type.Literal('hello')) + Assert.IsTrue(KindGuard.IsLiteral(T)) + Assert.IsEqual(T.const, 'Hello') + }) + it('Should guard for Capitalize 3', () => { + const T = Type.Capitalize(Type.Union([Type.Literal('hello'), Type.Literal('world')])) + Assert.IsTrue(KindGuard.IsUnion(T)) + Assert.IsEqual(T.anyOf[0].const, 'Hello') + Assert.IsEqual(T.anyOf[1].const, 'World') + }) + it('Should guard for Capitalize 4', () => { + const T = Type.Capitalize(Type.TemplateLiteral('hello${0|1}')) + Assert.IsTrue(KindGuard.IsTemplateLiteral(T)) + Assert.IsEqual(T.pattern, '^(Hello0|Hello1)$') + }) + it('Should guard for Capitalize 5', () => { + const T = Type.Capitalize(Type.TemplateLiteral([Type.Literal('hello'), Type.Union([Type.Literal(0), Type.Literal(1)])])) + Assert.IsTrue(KindGuard.IsTemplateLiteral(T)) + Assert.IsEqual(T.pattern, '^(Hello0|Hello1)$') + }) +}) diff --git a/test/runtime/type/guard/kind/composite.ts b/test/runtime/type/guard/kind/composite.ts new file mode 100644 index 000000000..bd709667e --- /dev/null +++ b/test/runtime/type/guard/kind/composite.ts @@ -0,0 +1,165 @@ +import { KindGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TComposite', () => { + it('Should guard for distinct properties', () => { + const T = Type.Composite([Type.Object({ x: Type.Number() }), Type.Object({ y: Type.Number() })]) + Assert.IsTrue(KindGuard.IsNumber(T.properties.x)) + Assert.IsTrue(KindGuard.IsNumber(T.properties.y)) + }) + it('Should guard for overlapping properties', () => { + const T = Type.Composite([Type.Object({ x: Type.Number() }), Type.Object({ x: Type.Number() })]) + Assert.IsTrue(KindGuard.IsIntersect(T.properties.x)) + // @ts-ignore + Assert.IsTrue(KindGuard.IsNumber(T.properties.x.allOf[0])) + // @ts-ignore + Assert.IsTrue(KindGuard.IsNumber(T.properties.x.allOf[1])) + }) + it('Should not produce optional property if all properties are not optional', () => { + const T = Type.Composite([Type.Object({ x: Type.Optional(Type.Number()) }), Type.Object({ x: Type.Number() })]) + Assert.IsFalse(KindGuard.IsOptional(T.properties.x)) + }) + // Note for: https://github.com/sinclairzx81/typebox/issues/419 + // Determining if a composite property is optional requires a deep check for all properties gathered during a indexed access + // call. Currently, there isn't a trivial way to perform this check without running into possibly infinite instantiation issues. + // The optional check is only specific to overlapping properties. Singular properties will continue to work as expected. The + // rule is "if all composite properties for a key are optional, then the composite property is optional". Defer this test and + // document as minor breaking change. + // + it('Should produce optional property if all composited properties are optional', () => { + // prettier-ignore + const T = Type.Composite([ + Type.Object({ x: Type.Optional(Type.Number()) }), + Type.Object({ x: Type.Optional(Type.Number()) }) + ]) + Assert.IsTrue(KindGuard.IsOptional(T.properties.x)) + Assert.IsEqual(T.required, undefined) + }) + // prettier-ignore + it('Should produce required property if some composited properties are not optional', () => { + const T = Type.Composite([ + Type.Object({ x: Type.Optional(Type.Number()) }), + Type.Object({ x: Type.Number() }) + ]) + Assert.IsFalse(KindGuard.IsOptional(T.properties.x)) + Assert.IsTrue(T.required!.includes('x')) + }) + // prettier-ignore + it('Should preserve single optional property', () => { + const T = Type.Composite([ + Type.Object({ x: Type.Optional(Type.Number()) }), + ]) + Assert.IsTrue(KindGuard.IsOptional(T.properties.x)) + Assert.IsEqual(T.required, undefined) + }) + // ---------------------------------------------------------------- + // Intersect + // ---------------------------------------------------------------- + // prettier-ignore + it('Should composite Intersect 1', () => { + const T = Type.Composite([ + Type.Intersect([ + Type.Object({ x: Type.Number() }), + Type.Object({ y: Type.Number() }), + ]), + Type.Intersect([ + Type.Object({ z: Type.Number() }), + ]) + ]) + Assert.IsEqual(T, Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number() + })) + }) + // prettier-ignore + it('Should composite Intersect 2', () => { + const T = Type.Composite([ + Type.Intersect([ + Type.Object({ x: Type.Number() }), + Type.Object({ x: Type.Number() }), + ]), + Type.Intersect([ + Type.Object({ x: Type.Number() }), + ]) + ]) + Assert.IsEqual(T, Type.Object({ + x: Type.Intersect([Type.Intersect([Type.Number(), Type.Number()]), Type.Number()]) + })) + }) + // prettier-ignore + it('Should composite Intersect 3', () => { + const T = Type.Composite([ + Type.Number(), + Type.Boolean() + ]) + Assert.IsEqual(T, Type.Object({})) + }) + // prettier-ignore + it('Should composite Intersect 4', () => { + const T = Type.Composite([ + Type.Number(), + Type.Boolean(), + Type.Object({ x: Type.String() }) + ]) + Assert.IsEqual(T, Type.Object({ + x: Type.String() + })) + }) + // prettier-ignore + it('Should composite Intersect 5', () => { + const T = Type.Composite([ + Type.Object({ x: Type.Optional(Type.String()) }), + Type.Object({ x: Type.String() }) + ]) + Assert.IsEqual(T, Type.Object({ + x: Type.Intersect([Type.String(), Type.String()]) + })) + }) + // prettier-ignore + it('Should composite Intersect 6', () => { + const T = Type.Composite([ + Type.Object({ x: Type.Optional(Type.String()) }), + Type.Object({ x: Type.Optional(Type.String()) }) + ]) + Assert.IsEqual(T, Type.Object({ + x: Type.Optional(Type.Intersect([Type.String(), Type.String()])) + })) + }) + // ---------------------------------------------------------------- + // Union + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/789 + // prettier-ignore + it('Should composite Union 1 (non-overlapping)', () => { + const T = Type.Composite([ + Type.Union([ + Type.Object({ x: Type.Number() }), + Type.Object({ y: Type.Number() }), + ]), + Type.Union([ + Type.Object({ z: Type.Number() }), + ]) + ]) + Assert.IsEqual(T, Type.Object({ + z: Type.Number() + })) + }) + // https://github.com/sinclairzx81/typebox/issues/789 + // prettier-ignore + it('Should composite Union 2 (overlapping)', () => { + const T = Type.Composite([ + Type.Union([ + Type.Object({ x: Type.Number() }), + Type.Object({ x: Type.Number() }), + ]), + Type.Union([ + Type.Object({ x: Type.Number() }), + ]) + ]) + Assert.IsEqual(T, Type.Object({ + x: Type.Intersect([Type.Union([Type.Number(), Type.Number()]), Type.Number()]) + })) + }) +}) diff --git a/test/runtime/type/guard/kind/computed.ts b/test/runtime/type/guard/kind/computed.ts new file mode 100644 index 000000000..5d7929af9 --- /dev/null +++ b/test/runtime/type/guard/kind/computed.ts @@ -0,0 +1,33 @@ +import { KindGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TComputed', () => { + // ---------------------------------------------------------------- + // Schema + // ---------------------------------------------------------------- + it('Should guard for Schema', () => { + const T = Type.Partial(Type.Ref('A')) + Assert.IsTrue(KindGuard.IsComputed(T)) + Assert.IsTrue(KindGuard.IsSchema(T)) + }) + // ---------------------------------------------------------------- + // Record + // ---------------------------------------------------------------- + it('Should guard for Record 1', () => { + const T = Type.Record(Type.String(), Type.String()) + Assert.IsTrue(KindGuard.IsRecord(T)) + }) + it('Should guard for Record 3', () => { + const T = Type.Record(Type.String(), Type.Ref('A')) + Assert.IsTrue(KindGuard.IsRecord(T)) + }) + it('Should guard for Record 3', () => { + const T = Type.Record(Type.String(), Type.Partial(Type.Ref('A'))) + Assert.IsTrue(KindGuard.IsRecord(T)) + }) + it('Should guard for Record 4', () => { + const T = Type.Record(Type.Ref('A'), Type.String()) + Assert.IsTrue(KindGuard.IsNever(T)) + }) +}) diff --git a/test/runtime/type/guard/kind/const.ts b/test/runtime/type/guard/kind/const.ts new file mode 100644 index 000000000..cb2115c92 --- /dev/null +++ b/test/runtime/type/guard/kind/const.ts @@ -0,0 +1,124 @@ +import { KindGuard, ValueGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TConstT', () => { + // ---------------------------------------------------------------- + // Identity Types + // ---------------------------------------------------------------- + it('Should guard for TConst 1', () => { + const T = Type.Const(undefined) + Assert.IsFalse(KindGuard.IsReadonly(T)) + Assert.IsTrue(KindGuard.IsUndefined(T)) + }) + it('Should guard for TConst 2', () => { + const T = Type.Const(null) + Assert.IsFalse(KindGuard.IsReadonly(T)) + Assert.IsTrue(KindGuard.IsNull(T)) + }) + it('Should guard for TConst 3', () => { + const T = Type.Const(Symbol()) + Assert.IsFalse(KindGuard.IsReadonly(T)) + Assert.IsTrue(KindGuard.IsSymbol(T)) + }) + it('Should guard for TConst 4', () => { + const T = Type.Const(1 as const) + Assert.IsFalse(KindGuard.IsReadonly(T)) + Assert.IsTrue(KindGuard.IsLiteral(T)) + Assert.IsEqual(T.const, 1) + }) + it('Should guard for TConst 5', () => { + const T = Type.Const('hello' as const) + Assert.IsFalse(KindGuard.IsReadonly(T)) + Assert.IsTrue(KindGuard.IsLiteral(T)) + Assert.IsEqual(T.const, 'hello') + }) + it('Should guard for TConst 6', () => { + const T = Type.Const(true as const) + Assert.IsFalse(KindGuard.IsReadonly(T)) + Assert.IsTrue(KindGuard.IsLiteral(T)) + Assert.IsEqual(T.const, true) + }) + // ---------------------------------------------------------------- + // Complex Types + // ---------------------------------------------------------------- + it('Should guard for TConst 7', () => { + const T = Type.Const(100n as const) + Assert.IsFalse(KindGuard.IsReadonly(T)) + // TS disparity because TLiteral does not support Bigint + Assert.IsTrue(KindGuard.IsBigInt(T)) + }) + it('Should guard for TConst 8', () => { + const T = Type.Const(new Date()) + Assert.IsFalse(KindGuard.IsReadonly(T)) + Assert.IsTrue(KindGuard.IsDate(T)) + }) + it('Should guard for TConst 9', () => { + const T = Type.Const(new Uint8Array()) + Assert.IsFalse(KindGuard.IsReadonly(T)) + Assert.IsTrue(KindGuard.IsUint8Array(T)) + }) + it('Should guard for TConst 10', () => { + const T = Type.Const(function () {}) + Assert.IsFalse(KindGuard.IsReadonly(T)) + Assert.IsTrue(KindGuard.IsFunction(T)) + Assert.IsTrue(T.parameters.length === 0) + Assert.IsTrue(KindGuard.IsUnknown(T.returns)) + }) + it('Should guard for TConst 11', () => { + const T = Type.Const(new (class {})()) + Assert.IsFalse(KindGuard.IsReadonly(T)) + Assert.IsTrue(KindGuard.IsObject(T)) + // Object types that are neither Date or Uint8Array evaluate as empty objects + Assert.IsEqual(T.properties, {}) + }) + it('Should guard for TConst 12', () => { + const T = Type.Const((function* (): any {})()) + const R = ValueGuard.IsIterator((function* (): any {})()) + Assert.IsFalse(KindGuard.IsReadonly(T)) + Assert.IsTrue(KindGuard.IsAny(T)) + }) + it('Should guard for TConst 13', () => { + const T = Type.Const((async function* (): any {})()) + const R = ValueGuard.IsAsyncIterator((function* (): any {})()) + Assert.IsFalse(KindGuard.IsReadonly(T)) + Assert.IsTrue(KindGuard.IsAny(T)) + }) + it('Should guard for TConst 14', () => { + const T = Type.Const({ + x: 1, + y: { + z: 2, + }, + } as const) + // root + Assert.IsFalse(KindGuard.IsReadonly(T)) + Assert.IsTrue(KindGuard.IsObject(T)) + // x + Assert.IsTrue(KindGuard.IsLiteral(T.properties.x)) + Assert.IsEqual(T.properties.x.const, 1) + // y + Assert.IsTrue(KindGuard.IsReadonly(T.properties.y)) + Assert.IsTrue(KindGuard.IsObject(T.properties.y)) + // y.z + Assert.IsTrue(KindGuard.IsReadonly(T.properties.y.properties.z)) + Assert.IsTrue(KindGuard.IsLiteral(T.properties.y.properties.z)) + Assert.IsEqual(T.properties.y.properties.z.const, 2) + }) + it('Should guard for TConst 15', () => { + const T = Type.Const([1, 2, 3] as const) + // root (arrays are always readonly as root) + Assert.IsTrue(KindGuard.IsReadonly(T)) + Assert.IsTrue(KindGuard.IsTuple(T)) + Assert.IsTrue(T.items?.length === 3) + // 0 + Assert.IsFalse(KindGuard.IsReadonly(T.items![0])) + Assert.IsTrue(KindGuard.IsLiteral(T.items![0])) + // 1 + Assert.IsFalse(KindGuard.IsReadonly(T.items![1])) + Assert.IsTrue(KindGuard.IsLiteral(T.items![1])) + // 2 + Assert.IsFalse(KindGuard.IsReadonly(T.items![2])) + Assert.IsTrue(KindGuard.IsLiteral(T.items![2])) + }) +}) diff --git a/test/runtime/type/guard/kind/constructor.ts b/test/runtime/type/guard/kind/constructor.ts new file mode 100644 index 000000000..d2146aa7c --- /dev/null +++ b/test/runtime/type/guard/kind/constructor.ts @@ -0,0 +1,14 @@ +import { KindGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TConstructor', () => { + it('Should guard for TConstructor', () => { + const R = KindGuard.IsConstructor(Type.Constructor([], Type.Number())) + Assert.IsTrue(R) + }) + it('Should not guard for TConstructor', () => { + const R = KindGuard.IsConstructor(null) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/guard/kind/date.ts b/test/runtime/type/guard/kind/date.ts new file mode 100644 index 000000000..5e2d252a7 --- /dev/null +++ b/test/runtime/type/guard/kind/date.ts @@ -0,0 +1,14 @@ +import { KindGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TDate', () => { + it('Should guard for TDate', () => { + const R = KindGuard.IsDate(Type.Date()) + Assert.IsTrue(R) + }) + it('Should not guard for TDate', () => { + const R = KindGuard.IsDate(null) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/guard/kind/enum.ts b/test/runtime/type/guard/kind/enum.ts new file mode 100644 index 000000000..b47631cfa --- /dev/null +++ b/test/runtime/type/guard/kind/enum.ts @@ -0,0 +1,143 @@ +import { KindGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TEnum', () => { + // ---------------------------------------------------------------- + // Options + // ---------------------------------------------------------------- + it('Should guard for Options 1', () => { + const T = Type.Enum({ x: 1 }, { extra: 'hello', $id: 'T' }) + Assert.IsEqual(T.extra, 'hello') + Assert.IsEqual(T.$id, 'T') + }) + it('Should guard for Options 2', () => { + enum E { + x, + } + const T = Type.Enum(E, { extra: 'hello', $id: 'T' }) + Assert.IsEqual(T.extra, 'hello') + Assert.IsEqual(T.$id, 'T') + }) + it('Should guard for Options 3', () => { + enum E {} + const T = Type.Enum(E, { extra: 'hello', $id: 'T' }) + Assert.IsEqual(T.extra, 'hello') + Assert.IsEqual(T.$id, 'T') + }) + it('Should guard for Options 4', () => { + const T = Type.Enum({}, { extra: 'hello', $id: 'T' }) + Assert.IsEqual(T.extra, 'hello') + Assert.IsEqual(T.$id, 'T') + }) + // ---------------------------------------------------------------- + // Empty + // ---------------------------------------------------------------- + it('Should guard for Empty 1', () => { + const T = Type.Enum({}) + Assert.IsTrue(KindGuard.IsNever(T)) + }) + it('Should guard for Empty 2', () => { + enum E {} + const T = Type.Enum(E) + Assert.IsTrue(KindGuard.IsNever(T)) + }) + + // ---------------------------------------------------------------- + // Enum + // ---------------------------------------------------------------- + it('Should guard for TEnum Enum 0', () => { + enum E {} + const T = Type.Enum(E) + Assert.IsTrue(KindGuard.IsNever(T)) + }) + it('Should guard for TEnum Enum 1', () => { + enum E { + A, + } + const T = Type.Enum(E) + Assert.IsTrue(KindGuard.IsLiteral(T)) + Assert.IsEqual(T.const, E.A) + }) + it('Should guard for TEnum Enum 2', () => { + enum E { + A = 1, + B = 2, + C = 3, + } + const T = Type.Enum(E) + Assert.IsTrue(KindGuard.IsUnion(T)) + Assert.IsEqual(T.anyOf[0].const, E.A) + Assert.IsEqual(T.anyOf[1].const, E.B) + Assert.IsEqual(T.anyOf[2].const, E.C) + }) + it('Should guard for TEnum Enum 3', () => { + enum E { + A = 'X', + B = 'Y', + C = 'Z', + } + const T = Type.Enum(E) + Assert.IsTrue(KindGuard.IsUnion(T)) + Assert.IsEqual(T.anyOf[0].const, E.A) + Assert.IsEqual(T.anyOf[1].const, E.B) + Assert.IsEqual(T.anyOf[2].const, E.C) + }) + it('Should guard for TEnum Enum 4', () => { + enum E { + A = 'X', + B = 'Y', + C = 'X', + } + const T = Type.Enum(E) + Assert.IsTrue(KindGuard.IsUnion(T)) + Assert.IsEqual(T.anyOf[0].const, E.A) + Assert.IsEqual(T.anyOf[1].const, E.B) + Assert.IsEqual(T.anyOf.length, 2) + }) + // ---------------------------------------------------------------- + // Object Literal + // ---------------------------------------------------------------- + it('Should guard for TEnum Object Literal 0', () => { + const T = Type.Enum({}) + Assert.IsTrue(KindGuard.IsNever(T)) + }) + it('Should guard for TEnum Object Literal 1', () => { + const T = Type.Enum({ A: 1 }) + Assert.IsTrue(KindGuard.IsLiteral(T)) + Assert.IsEqual(T.const, 1) + }) + it('Should guard for TEnum Object Literal 2', () => { + const T = Type.Enum({ + A: 1, + B: 2, + C: 3, + }) + Assert.IsTrue(KindGuard.IsUnion(T)) + Assert.IsEqual(T.anyOf[0].const, 1) + Assert.IsEqual(T.anyOf[1].const, 2) + Assert.IsEqual(T.anyOf[2].const, 3) + }) + it('Should guard for TEnum Object Literal 3', () => { + const T = Type.Enum({ + A: 'X', + B: 'Y', + C: 'Z', + }) + Assert.IsTrue(KindGuard.IsUnion(T)) + Assert.IsEqual(T.anyOf[0].const, 'X') + Assert.IsEqual(T.anyOf[1].const, 'Y') + Assert.IsEqual(T.anyOf[2].const, 'Z') + }) + it('Should guard for TEnum Object Literal 4', () => { + const T = Type.Enum({ + A: 'X', + B: 'Y', + C: 'X', + }) + Assert.IsTrue(KindGuard.IsUnion(T)) + Assert.IsEqual(T.anyOf[0].const, 'X') + Assert.IsEqual(T.anyOf[1].const, 'Y') + Assert.IsEqual(T.anyOf.length, 2) + }) +}) diff --git a/test/runtime/type/guard/kind/exclude.ts b/test/runtime/type/guard/kind/exclude.ts new file mode 100644 index 000000000..d3a19af16 --- /dev/null +++ b/test/runtime/type/guard/kind/exclude.ts @@ -0,0 +1,96 @@ +import { KindGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TExclude', () => { + it('Should exclude string from number', () => { + const T = Type.Exclude(Type.String(), Type.Number()) + Assert.IsTrue(KindGuard.IsString(T)) + }) + it('Should exclude string from string', () => { + const T = Type.Exclude(Type.String(), Type.String()) + Assert.IsTrue(KindGuard.IsNever(T)) + }) + it('Should exclude string | number | boolean from string', () => { + const T = Type.Exclude(Type.Union([Type.String(), Type.Number(), Type.Boolean()]), Type.String()) + Assert.IsTrue(KindGuard.IsUnion(T)) + Assert.IsTrue(KindGuard.IsNumber(T.anyOf[0])) + Assert.IsTrue(KindGuard.IsBoolean(T.anyOf[1])) + }) + it('Should exclude string | number | boolean from string | boolean', () => { + const T = Type.Exclude(Type.Union([Type.String(), Type.Number(), Type.Boolean()]), Type.Union([Type.String(), Type.Boolean()])) + Assert.IsTrue(KindGuard.IsNumber(T)) + }) + // ------------------------------------------------------------------------ + // TemplateLiteral | TemplateLiteral + // ------------------------------------------------------------------------ + it('Should exclude TemplateLiteral | TemplateLiteral 1', () => { + const A = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + const B = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + const T = Type.Exclude(A, B) + Assert.IsTrue(KindGuard.IsNever(T)) + }) + it('Should exclude TemplateLiteral | TemplateLiteral 1', () => { + const A = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + const B = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B')])]) + const T = Type.Exclude(A, B) + Assert.IsTrue(['C'].includes(T.const)) + }) + it('Should exclude TemplateLiteral | TemplateLiteral 1', () => { + const A = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + const B = Type.TemplateLiteral([Type.Union([Type.Literal('A')])]) + const T = Type.Exclude(A, B) + Assert.IsTrue(['C', 'B'].includes(T.anyOf[0].const)) + Assert.IsTrue(['C', 'B'].includes(T.anyOf[1].const)) + }) + // ------------------------------------------------------------------------ + // TemplateLiteral | Union 1 + // ------------------------------------------------------------------------ + it('Should exclude TemplateLiteral | Union 1', () => { + const A = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + const B = Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')]) + const T = Type.Exclude(A, B) + Assert.IsTrue(KindGuard.IsNever(T)) + }) + it('Should exclude TemplateLiteral | Union 1', () => { + const A = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + const B = Type.Union([Type.Literal('A'), Type.Literal('B')]) + const T = Type.Exclude(A, B) + Assert.IsTrue(['C'].includes(T.const)) + }) + it('Should exclude TemplateLiteral | Union 1', () => { + const A = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + const B = Type.Union([Type.Literal('A')]) + const T = Type.Exclude(A, B) + Assert.IsTrue(['C', 'B'].includes(T.anyOf[0].const)) + Assert.IsTrue(['C', 'B'].includes(T.anyOf[1].const)) + }) + // ------------------------------------------------------------------------ + // Union | TemplateLiteral 1 + // ------------------------------------------------------------------------ + it('Should exclude Union | TemplateLiteral 1', () => { + const A = Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')]) + const B = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + const T = Type.Exclude(A, B) + Assert.IsTrue(KindGuard.IsNever(T)) + }) + it('Should exclude Union | TemplateLiteral 1', () => { + const A = Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')]) + const B = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B')])]) + const T = Type.Exclude(A, B) + Assert.IsTrue(['C'].includes(T.const)) + }) + it('Should exclude Union | TemplateLiteral 1', () => { + const A = Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')]) + const B = Type.TemplateLiteral([Type.Union([Type.Literal('A')])]) + const T = Type.Exclude(A, B) + Assert.IsTrue(['C', 'B'].includes(T.anyOf[0].const)) + Assert.IsTrue(['C', 'B'].includes(T.anyOf[1].const)) + }) + it('Should exclude with options', () => { + const A = Type.String() + const B = Type.String() + const T = Type.Exclude(A, B, { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) +}) diff --git a/test/runtime/type/guard/kind/extract.ts b/test/runtime/type/guard/kind/extract.ts new file mode 100644 index 000000000..54f406380 --- /dev/null +++ b/test/runtime/type/guard/kind/extract.ts @@ -0,0 +1,102 @@ +import { KindGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TExtract', () => { + it('Should extract string from number', () => { + const T = Type.Extract(Type.String(), Type.Number()) + Assert.IsTrue(KindGuard.IsNever(T)) + }) + it('Should extract string from string', () => { + const T = Type.Extract(Type.String(), Type.String()) + Assert.IsTrue(KindGuard.IsString(T)) + }) + it('Should extract string | number | boolean from string', () => { + const T = Type.Extract(Type.Union([Type.String(), Type.Number(), Type.Boolean()]), Type.String()) + Assert.IsTrue(KindGuard.IsString(T)) + }) + it('Should extract string | number | boolean from string | boolean', () => { + const T = Type.Extract(Type.Union([Type.String(), Type.Number(), Type.Boolean()]), Type.Union([Type.String(), Type.Boolean()])) + Assert.IsTrue(KindGuard.IsUnion(T)) + Assert.IsTrue(KindGuard.IsString(T.anyOf[0])) + Assert.IsTrue(KindGuard.IsBoolean(T.anyOf[1])) + }) + // ------------------------------------------------------------------------ + // TemplateLiteral | TemplateLiteral + // ------------------------------------------------------------------------ + it('Should extract TemplateLiteral | TemplateLiteral 1', () => { + const A = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + const B = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + const T = Type.Extract(A, B) + Assert.IsTrue(['A', 'B', 'C'].includes(T.anyOf[0].const)) + Assert.IsTrue(['A', 'B', 'C'].includes(T.anyOf[1].const)) + Assert.IsTrue(['A', 'B', 'C'].includes(T.anyOf[2].const)) + }) + it('Should extract TemplateLiteral | TemplateLiteral 1', () => { + const A = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + const B = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B')])]) + const T = Type.Extract(A, B) + Assert.IsTrue(['A', 'B'].includes(T.anyOf[0].const)) + Assert.IsTrue(['A', 'B'].includes(T.anyOf[1].const)) + }) + it('Should extract TemplateLiteral | TemplateLiteral 1', () => { + const A = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + const B = Type.TemplateLiteral([Type.Union([Type.Literal('A')])]) + const T = Type.Extract(A, B) + Assert.IsTrue(['A'].includes(T.const)) + }) + // ------------------------------------------------------------------------ + // TemplateLiteral | Union 1 + // ------------------------------------------------------------------------ + it('Should extract TemplateLiteral | Union 1', () => { + const A = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + const B = Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')]) + const T = Type.Extract(A, B) + Assert.IsTrue(['A', 'B', 'C'].includes(T.anyOf[0].const)) + Assert.IsTrue(['A', 'B', 'C'].includes(T.anyOf[1].const)) + Assert.IsTrue(['A', 'B', 'C'].includes(T.anyOf[2].const)) + }) + it('Should extract TemplateLiteral | Union 1', () => { + const A = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + const B = Type.Union([Type.Literal('A'), Type.Literal('B')]) + const T = Type.Extract(A, B) + Assert.IsTrue(['A', 'B'].includes(T.anyOf[0].const)) + Assert.IsTrue(['A', 'B'].includes(T.anyOf[1].const)) + }) + it('Should extract TemplateLiteral | Union 1', () => { + const A = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + const B = Type.Union([Type.Literal('A')]) + const T = Type.Extract(A, B) + Assert.IsTrue(['A'].includes(T.const)) + }) + // ------------------------------------------------------------------------ + // Union | TemplateLiteral 1 + // ------------------------------------------------------------------------ + it('Should extract Union | TemplateLiteral 1', () => { + const A = Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')]) + const B = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + const T = Type.Extract(A, B) + Assert.IsTrue(['A', 'B', 'C'].includes(T.anyOf[0].const)) + Assert.IsTrue(['A', 'B', 'C'].includes(T.anyOf[1].const)) + Assert.IsTrue(['A', 'B', 'C'].includes(T.anyOf[2].const)) + }) + it('Should extract Union | TemplateLiteral 1', () => { + const A = Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')]) + const B = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B')])]) + const T = Type.Extract(A, B) + Assert.IsTrue(['A', 'B'].includes(T.anyOf[0].const)) + Assert.IsTrue(['A', 'B'].includes(T.anyOf[1].const)) + }) + it('Should extract Union | TemplateLiteral 1', () => { + const A = Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')]) + const B = Type.TemplateLiteral([Type.Union([Type.Literal('A')])]) + const T = Type.Extract(A, B) + Assert.IsTrue(['A'].includes(T.const)) + }) + it('Should extract with options', () => { + const A = Type.String() + const B = Type.String() + const T = Type.Extract(A, B, { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) +}) diff --git a/test/runtime/type/guard/kind/function.ts b/test/runtime/type/guard/kind/function.ts new file mode 100644 index 000000000..cf868c71d --- /dev/null +++ b/test/runtime/type/guard/kind/function.ts @@ -0,0 +1,14 @@ +import { KindGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TFunction', () => { + it('Should guard for TFunction', () => { + const R = KindGuard.IsFunction(Type.Function([], Type.Number())) + Assert.IsTrue(R) + }) + it('Should not guard for TFunction', () => { + const R = KindGuard.IsFunction(null) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/guard/kind/import.ts b/test/runtime/type/guard/kind/import.ts new file mode 100644 index 000000000..03921fe9f --- /dev/null +++ b/test/runtime/type/guard/kind/import.ts @@ -0,0 +1,291 @@ +import { KindGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TImport', () => { + it('Should guard for TImport', () => { + const Module = Type.Module({ + A: Type.String(), + }) + const A = Module.Import('A') + const N = A.$defs[A.$ref] + Assert.IsTrue(KindGuard.IsImport(A)) + Assert.IsTrue(KindGuard.IsString(N)) + }) + // ---------------------------------------------------------------- + // Computed: Options + // ---------------------------------------------------------------- + it('Should guard for TImport with Options 1', () => { + const Module = Type.Module({ + A: Type.String(), + }) + const A = Module.Import('A', { format: 'string' }) + const N = A.$defs[A.$ref] + Assert.IsTrue(KindGuard.IsImport(A)) + Assert.IsTrue(KindGuard.IsString(N)) + Assert.IsTrue(N.format === 'string') + }) + it('Should guard for TImport with Options 2', () => { + const Module = Type.Module({ + R: Type.Object({ x: Type.Number() }), + A: Type.Ref('R'), + }) + const A = Module.Import('A', { test: 'test' }) + const N = A.$defs[A.$ref] + Assert.IsTrue(KindGuard.IsImport(A)) + Assert.IsTrue(KindGuard.IsRef(N)) + Assert.IsTrue(N.test === 'test') + }) + it('Should guard for TImport with Options 3', () => { + const Module = Type.Module({ + R: Type.Object({ x: Type.Number() }), + A: Type.Partial(Type.Ref('R')), + }) + const A = Module.Import('A', { additionalProperties: false }) + const N = A.$defs[A.$ref] + Assert.IsTrue(KindGuard.IsImport(A)) + Assert.IsTrue(KindGuard.IsObject(N)) + Assert.IsTrue(N.additionalProperties === false) + }) + // ---------------------------------------------------------------- + // Computed: Awaited + // ---------------------------------------------------------------- + it('Should compute for Awaited', () => { + const Module = Type.Module({ + T: Type.Promise(Type.String()), + R: Type.Awaited(Type.Ref('T')), + }) + const T = Module.Import('R') + Assert.IsTrue(KindGuard.IsString(T.$defs['R'])) + }) + // ---------------------------------------------------------------- + // Computed: Index (Note: Pending Reimplementation of Index) + // ---------------------------------------------------------------- + it('Should compute for Index 2', () => { + const Module = Type.Module({ + T: Type.Object({ x: Type.Number(), y: Type.String() }), + I: Type.Literal('x'), + R: Type.Index(Type.Ref('T'), Type.Ref('I')) as never, // fail + }) + const T = Module.Import('R') + Assert.IsTrue(KindGuard.IsNumber(T.$defs['R'])) + }) + // ---------------------------------------------------------------- + // Computed: Omit + // ---------------------------------------------------------------- + it('Should compute for Omit 1', () => { + const Module = Type.Module({ + T: Type.Object({ x: Type.Number(), y: Type.Number() }), + R: Type.Omit(Type.Ref('T'), Type.Literal('x')), + }) + const T = Module.Import('R') + Assert.IsTrue(KindGuard.IsObject(T.$defs['R'])) + Assert.IsTrue(KindGuard.IsNumber(T.$defs['R'].properties.y)) + // @ts-ignore + Assert.IsTrue(T.$defs['R'].properties.x === undefined) + }) + it('Should compute for Omit 2', () => { + const Module = Type.Module({ + T: Type.Object({ x: Type.Number(), y: Type.Number() }), + K: Type.Literal('x'), + R: Type.Omit(Type.Ref('T'), Type.Ref('K')), + }) + const T = Module.Import('R') + Assert.IsTrue(KindGuard.IsObject(T.$defs['R'])) + Assert.IsTrue(KindGuard.IsNumber(T.$defs['R'].properties.y)) + // @ts-ignore + Assert.IsTrue(T.$defs['R'].properties.x === undefined) + }) + // ---------------------------------------------------------------- + // Computed: Partial + // ---------------------------------------------------------------- + it('Should compute for Partial', () => { + const Module = Type.Module({ + T: Type.Object({ x: Type.Number() }), + R: Type.Partial(Type.Ref('T')), + }) + const T = Module.Import('R') + Assert.IsTrue(KindGuard.IsObject(T.$defs['R'])) + Assert.IsTrue(KindGuard.IsNumber(T.$defs['R'].properties.x)) + Assert.IsTrue(KindGuard.IsOptional(T.$defs['R'].properties.x)) + }) + // ---------------------------------------------------------------- + // Computed: Pick + // ---------------------------------------------------------------- + it('Should compute for Pick 1', () => { + const Module = Type.Module({ + T: Type.Object({ x: Type.Number(), y: Type.Number() }), + R: Type.Pick(Type.Ref('T'), Type.Literal('x')), + }) + const T = Module.Import('R') + Assert.IsTrue(KindGuard.IsObject(T.$defs['R'])) + Assert.IsTrue(KindGuard.IsNumber(T.$defs['R'].properties.x)) + // @ts-ignore + Assert.IsTrue(T.$defs['R'].properties.y === undefined) + }) + it('Should compute for Pick 2', () => { + const Module = Type.Module({ + T: Type.Object({ x: Type.Number(), y: Type.Number() }), + K: Type.Literal('x'), + R: Type.Pick(Type.Ref('T'), Type.Ref('K')), + }) + const T = Module.Import('R') + Assert.IsTrue(KindGuard.IsObject(T.$defs['R'])) + Assert.IsTrue(KindGuard.IsNumber(T.$defs['R'].properties.x)) + // @ts-ignore + Assert.IsTrue(T.$defs['R'].properties.y === undefined) + }) + // ---------------------------------------------------------------- + // Computed: Record + // ---------------------------------------------------------------- + it('Should compute for Record 1', () => { + const Module = Type.Module({ + T: Type.Object({ x: Type.Number(), y: Type.String() }), + R: Type.Record(Type.String(), Type.Ref('T')), + }) + const T = Module.Import('R') + Assert.IsTrue(KindGuard.IsRecord(T.$defs['R'])) + // note: TRecord> are not computed. Only the Key is + // computed as TypeBox needs to make a deferred call to transform from + // TRecord to TObject for finite keys. + Assert.IsTrue(KindGuard.IsRef(T.$defs['R'].patternProperties['^(.*)$'])) + Assert.IsTrue(T.$defs['R'].patternProperties['^(.*)$'].$ref === 'T') + }) + it('Should compute for Record 2', () => { + const Module = Type.Module({ + T: Type.Number(), + R: Type.Record(Type.Union([Type.Literal('x'), Type.Literal('y')]), Type.Ref('T')), + }) + // Retain reference if not computed + const T = Module.Import('R') + Assert.IsTrue(KindGuard.IsObject(T.$defs['R'])) + Assert.IsTrue(KindGuard.IsRef(T.$defs['R'].properties.x)) + Assert.IsTrue(KindGuard.IsRef(T.$defs['R'].properties.y)) + }) + it('Should compute for Record 3', () => { + const Module = Type.Module({ + T: Type.Object({ x: Type.Number() }), + R: Type.Record(Type.Union([Type.Literal('x'), Type.Literal('y')]), Type.Partial(Type.Ref('T'))), + }) + // Dereference if computed + const T = Module.Import('R') + Assert.IsTrue(KindGuard.IsObject(T.$defs['R'])) + Assert.IsTrue(KindGuard.IsObject(T.$defs['R'].properties.x)) + Assert.IsTrue(KindGuard.IsObject(T.$defs['R'].properties.y)) + }) + // ---------------------------------------------------------------- + // Computed: Required + // ---------------------------------------------------------------- + it('Should compute for Required', () => { + const Module = Type.Module({ + T: Type.Partial(Type.Object({ x: Type.Number() })), + R: Type.Required(Type.Ref('T')), + }) + const T = Module.Import('R') + Assert.IsTrue(KindGuard.IsObject(T.$defs['R'])) + Assert.IsTrue(KindGuard.IsNumber(T.$defs['R'].properties.x)) + Assert.IsFalse(KindGuard.IsOptional(T.$defs['R'].properties.x)) + }) + // ---------------------------------------------------------------- + // Computed: KeyOf + // ---------------------------------------------------------------- + it('Should compute for KeyOf', () => { + const Module = Type.Module({ + T: Type.Object({ x: Type.Number(), y: Type.String() }), + R: Type.KeyOf(Type.Ref('T')), + }) + const T = Module.Import('R') + Assert.IsTrue(KindGuard.IsUnion(T.$defs['R'])) + Assert.IsTrue(KindGuard.IsLiteral(T.$defs['R'].anyOf[0])) + Assert.IsTrue(KindGuard.IsLiteral(T.$defs['R'].anyOf[1])) + Assert.IsTrue(T.$defs['R'].anyOf[0].const === 'x') + Assert.IsTrue(T.$defs['R'].anyOf[1].const === 'y') + }) + // ---------------------------------------------------------------- + // Modifiers: 1 + // ---------------------------------------------------------------- + it('Should compute for Modifiers 1', () => { + const Module = Type.Module({ + T: Type.Object({ + x: Type.ReadonlyOptional(Type.Null()), + y: Type.Readonly(Type.Null()), + z: Type.Optional(Type.Null()), + w: Type.Null(), + }), + }) + const T = Module.Import('T') + const R = T.$defs[T.$ref] + Assert.IsTrue(KindGuard.IsObject(R)) + + Assert.IsTrue(KindGuard.IsNull(R.properties.x)) + Assert.IsTrue(KindGuard.IsReadonly(R.properties.x)) + Assert.IsTrue(KindGuard.IsOptional(R.properties.x)) + + Assert.IsTrue(KindGuard.IsNull(R.properties.y)) + Assert.IsTrue(KindGuard.IsReadonly(R.properties.y)) + Assert.IsFalse(KindGuard.IsOptional(R.properties.y)) + + Assert.IsTrue(KindGuard.IsNull(R.properties.z)) + Assert.IsTrue(KindGuard.IsOptional(R.properties.z)) + Assert.IsFalse(KindGuard.IsReadonly(R.properties.z)) + + Assert.IsTrue(KindGuard.IsNull(R.properties.w)) + Assert.IsFalse(KindGuard.IsOptional(R.properties.w)) + Assert.IsFalse(KindGuard.IsReadonly(R.properties.w)) + }) + // ---------------------------------------------------------------- + // Modifiers: 2 + // ---------------------------------------------------------------- + it('Should compute for Modifiers 2', () => { + const Module = Type.Module({ + T: Type.Object({ + x: Type.ReadonlyOptional(Type.Array(Type.Null())), + y: Type.Readonly(Type.Array(Type.Null())), + z: Type.Optional(Type.Array(Type.Null())), + w: Type.Array(Type.Null()), + }), + }) + const T = Module.Import('T') + const R = T.$defs[T.$ref] + Assert.IsTrue(KindGuard.IsObject(R)) + + Assert.IsTrue(KindGuard.IsArray(R.properties.x)) + Assert.IsTrue(KindGuard.IsNull(R.properties.x.items)) + Assert.IsTrue(KindGuard.IsReadonly(R.properties.x)) + Assert.IsTrue(KindGuard.IsOptional(R.properties.x)) + + Assert.IsTrue(KindGuard.IsArray(R.properties.y)) + Assert.IsTrue(KindGuard.IsNull(R.properties.y.items)) + Assert.IsTrue(KindGuard.IsReadonly(R.properties.y)) + Assert.IsFalse(KindGuard.IsOptional(R.properties.y)) + + Assert.IsTrue(KindGuard.IsArray(R.properties.z)) + Assert.IsTrue(KindGuard.IsNull(R.properties.z.items)) + Assert.IsTrue(KindGuard.IsOptional(R.properties.z)) + Assert.IsFalse(KindGuard.IsReadonly(R.properties.z)) + + Assert.IsTrue(KindGuard.IsArray(R.properties.w)) + Assert.IsTrue(KindGuard.IsNull(R.properties.w.items)) + Assert.IsFalse(KindGuard.IsOptional(R.properties.w)) + Assert.IsFalse(KindGuard.IsReadonly(R.properties.w)) + }) + // ---------------------------------------------------------------- + // Modifiers: 3 + // ---------------------------------------------------------------- + it('Should compute for Modifiers 3', () => { + const Module = Type.Module({ + T: Type.Object({ + x: Type.Array(Type.Null()), + }), + // Computed Partial + U: Type.Partial(Type.Ref('T')), + }) + const T = Module.Import('U') + const R = T.$defs[T.$ref] + Assert.IsTrue(KindGuard.IsObject(R)) + + Assert.IsTrue(KindGuard.IsArray(R.properties.x)) + Assert.IsTrue(KindGuard.IsNull(R.properties.x.items)) + Assert.IsTrue(KindGuard.IsOptional(R.properties.x)) + }) +}) diff --git a/test/runtime/type/guard/kind/index.ts b/test/runtime/type/guard/kind/index.ts new file mode 100644 index 000000000..d8e59fb7d --- /dev/null +++ b/test/runtime/type/guard/kind/index.ts @@ -0,0 +1,54 @@ +import './any' +import './argument' +import './array' +import './async-iterator' +import './awaited' +import './bigint' +import './boolean' +import './capitalize' +import './composite' +import './computed' +import './const' +import './constructor' +import './date' +import './enum' +import './exclude' +import './extract' +import './function' +import './import' +import './indexed' +import './integer' +import './intersect' +import './iterator' +import './keyof' +import './kind' +import './literal' +import './lowercase' +import './mapped' +import './not' +import './null' +import './number' +import './object' +import './omit' +import './partial' +import './pick' +import './promise' +import './record' +import './recursive' +import './ref' +import './regexp' +import './required' +import './rest' +import './string' +import './symbol' +import './template-literal' +import './this' +import './tuple' +import './uint8array' +import './uncapitalize' +import './undefined' +import './union' +import './unknown' +import './unsafe' +import './uppercase' +import './void' diff --git a/test/runtime/type/guard/kind/indexed.ts b/test/runtime/type/guard/kind/indexed.ts new file mode 100644 index 000000000..4fc18ad3f --- /dev/null +++ b/test/runtime/type/guard/kind/indexed.ts @@ -0,0 +1,327 @@ +import { KindGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TIndex', () => { + it('Should Index 1', () => { + const T = Type.Object({ + x: Type.Number(), + y: Type.String(), + }) + const I = Type.Index(T, ['x']) + Assert.IsTrue(KindGuard.IsNumber(I)) + }) + it('Should Index 2', () => { + const T = Type.Object({ + x: Type.Number(), + y: Type.String(), + }) + const I = Type.Index(T, ['x', 'y']) + Assert.IsTrue(KindGuard.IsUnion(I)) + Assert.IsTrue(KindGuard.IsNumber(I.anyOf[0])) + Assert.IsTrue(KindGuard.IsString(I.anyOf[1])) + }) + it('Should Index 3', () => { + const T = Type.Object({ + x: Type.Number(), + y: Type.String(), + }) + const I = Type.Index(T, Type.KeyOf(T)) + Assert.IsTrue(KindGuard.IsUnion(I)) + Assert.IsTrue(KindGuard.IsNumber(I.anyOf[0])) + Assert.IsTrue(KindGuard.IsString(I.anyOf[1])) + }) + it('Should Index 4', () => { + const T = Type.Object({ + ab: Type.Number(), + ac: Type.String(), + }) + const I = Type.Index(T, Type.TemplateLiteral([Type.Literal('a'), Type.Union([Type.Literal('b'), Type.Literal('c')])])) + Assert.IsTrue(KindGuard.IsUnion(I)) + Assert.IsTrue(KindGuard.IsNumber(I.anyOf[0])) + Assert.IsTrue(KindGuard.IsString(I.anyOf[1])) + }) + it('Should Index 5', () => { + const T = Type.Intersect([Type.Object({ x: Type.Number() }), Type.Object({ y: Type.String() })]) + const I = Type.Index(T, ['x', 'y']) + Assert.IsTrue(KindGuard.IsUnion(I)) + Assert.IsTrue(KindGuard.IsNumber(I.anyOf[0])) + Assert.IsTrue(KindGuard.IsString(I.anyOf[1])) + }) + it('Should Index 6', () => { + const T = Type.Union([Type.Object({ x: Type.Number() }), Type.Object({ x: Type.String() })]) + const I = Type.Index(T, ['x']) + Assert.IsTrue(KindGuard.IsUnion(I)) + Assert.IsTrue(KindGuard.IsNumber(I.anyOf[0])) + Assert.IsTrue(KindGuard.IsString(I.anyOf[1])) + }) + it('Should Index 7', () => { + const T = Type.Array(Type.Null()) + const I = Type.Index(T, Type.Number()) + Assert.IsTrue(KindGuard.IsNull(I)) + }) + it('Should Index 6', () => { + const T = Type.Tuple([Type.Literal('hello'), Type.Literal('world')]) + const I = Type.Index(T, [0]) + Assert.IsTrue(KindGuard.IsLiteralString(I)) + Assert.IsEqual(I.const, 'hello') + }) + it('Should Index 8', () => { + const T = Type.Tuple([Type.Literal('hello'), Type.Literal('world')]) + const I = Type.Index(T, [1]) + Assert.IsTrue(KindGuard.IsLiteralString(I)) + Assert.IsEqual(I.const, 'world') + }) + it('Should Index 9', () => { + const T = Type.Tuple([Type.Literal('hello'), Type.Literal('world')]) + const I = Type.Index(T, [0, 1]) + Assert.IsTrue(KindGuard.IsUnion(I)) + Assert.IsEqual(I.anyOf[0].const, 'hello') + Assert.IsEqual(I.anyOf[1].const, 'world') + }) + it('Should Index 10', () => { + const T = Type.Tuple([Type.Literal('hello'), Type.Literal('world')]) + const I = Type.Index(T, [1, 0]) + Assert.IsTrue(KindGuard.IsUnion(I)) + Assert.IsEqual(I.anyOf[0].const, 'world') + Assert.IsEqual(I.anyOf[1].const, 'hello') + }) + it('Should Index 11', () => { + const T = Type.Tuple([Type.Literal('hello'), Type.Literal('world')]) + const I = Type.Index(T, [0, 0, 0, 1]) + Assert.IsTrue(KindGuard.IsUnion(I)) + Assert.IsEqual(I.anyOf[0].const, 'hello') + Assert.IsEqual(I.anyOf[1].const, 'hello') + Assert.IsEqual(I.anyOf[2].const, 'hello') + Assert.IsEqual(I.anyOf[3].const, 'world') + }) + it('Should Index 12', () => { + const T = Type.Tuple([Type.String(), Type.Boolean()]) + const I = Type.Index(T, Type.Number()) + Assert.IsTrue(KindGuard.IsUnion(I)) + Assert.IsTrue(KindGuard.IsString(I.anyOf[0])) + Assert.IsTrue(KindGuard.IsBoolean(I.anyOf[1])) + }) + it('Should Index 13', () => { + const T = Type.Tuple([Type.String()]) + const I = Type.Index(T, Type.Number()) + Assert.IsTrue(KindGuard.IsString(I)) + }) + it('Should Index 14', () => { + const T = Type.Tuple([]) + const I = Type.Index(T, Type.Number()) + Assert.IsTrue(KindGuard.IsNever(I)) + }) + it('Should Index 15', () => { + const T = Type.Object({ + 0: Type.Number(), + }) + const I = Type.Index(T, Type.Literal(0)) + Assert.IsTrue(KindGuard.IsNumber(I)) + }) + it('Should Index 16', () => { + const T = Type.Object({ + 0: Type.Number(), + }) + const I = Type.Index(T, Type.Literal('0')) + Assert.IsTrue(KindGuard.IsNumber(I)) + }) + it('Should Index 17', () => { + const T = Type.Object({ + '0': Type.Number(), + }) + const I = Type.Index(T, Type.Literal(0)) + Assert.IsTrue(KindGuard.IsNumber(I)) + }) + it('Should Index 18', () => { + const T = Type.Object({ + '0': Type.Number(), + }) + const I = Type.Index(T, Type.Literal('0')) + Assert.IsTrue(KindGuard.IsNumber(I)) + }) + it('Should Index 19', () => { + const T = Type.Object({ + 0: Type.Number(), + 1: Type.String(), + 2: Type.Boolean(), + }) + const I = Type.Index(T, Type.Union([Type.Literal(0), Type.Literal(2)])) + Assert.IsTrue(KindGuard.IsUnion(I)) + Assert.IsTrue(KindGuard.IsNumber(I.anyOf[0])) + Assert.IsTrue(KindGuard.IsBoolean(I.anyOf[1])) + }) + it('Should Index 20', () => { + const T = Type.Object({ + 0: Type.Number(), + 1: Type.String(), + 2: Type.Boolean(), + }) + const I = Type.Index(T, Type.BigInt()) + Assert.IsTrue(KindGuard.IsNever(I)) + }) + it('Should Index 21', () => { + const T = Type.Object({ + 0: Type.Number(), + 1: Type.String(), + 2: Type.Boolean(), + }) + const I = Type.Index(T, Type.Object({})) + Assert.IsTrue(KindGuard.IsNever(I)) + }) + it('Should Index 22', () => { + const A = Type.Object({ x: Type.Literal('A') }) + const B = Type.Object({ x: Type.Literal('B') }) + const C = Type.Object({ x: Type.Literal('C') }) + const D = Type.Object({ x: Type.Literal('D') }) + const T = Type.Intersect([A, B, C, D]) + const I = Type.Index(T, ['x']) + Assert.IsTrue(KindGuard.IsIntersect(I)) + Assert.IsTrue(KindGuard.IsLiteral(I.allOf[0])) + Assert.IsTrue(KindGuard.IsLiteral(I.allOf[1])) + Assert.IsTrue(KindGuard.IsLiteral(I.allOf[2])) + Assert.IsTrue(KindGuard.IsLiteral(I.allOf[3])) + }) + it('Should Index 23', () => { + const A = Type.Object({ x: Type.Literal('A') }) + const B = Type.Object({ x: Type.Literal('B') }) + const C = Type.Object({ x: Type.Literal('C') }) + const D = Type.Object({ x: Type.Literal('D') }) + const T = Type.Union([A, B, C, D]) + const I = Type.Index(T, ['x']) + Assert.IsTrue(KindGuard.IsUnion(I)) + Assert.IsTrue(KindGuard.IsLiteral(I.anyOf[0])) + Assert.IsTrue(KindGuard.IsLiteral(I.anyOf[1])) + Assert.IsTrue(KindGuard.IsLiteral(I.anyOf[2])) + Assert.IsTrue(KindGuard.IsLiteral(I.anyOf[3])) + }) + it('Should Index 24', () => { + const A = Type.Object({ x: Type.Literal('A'), y: Type.Number() }) + const B = Type.Object({ x: Type.Literal('B') }) + const C = Type.Object({ x: Type.Literal('C') }) + const D = Type.Object({ x: Type.Literal('D') }) + const T = Type.Intersect([A, B, C, D]) + const I = Type.Index(T, ['x', 'y']) + Assert.IsTrue(KindGuard.IsUnion(I)) + Assert.IsTrue(KindGuard.IsIntersect(I.anyOf[0])) + Assert.IsTrue(KindGuard.IsNumber(I.anyOf[1])) + }) + it('Should Index 25', () => { + const A = Type.Object({ x: Type.Literal('A'), y: Type.Number() }) + const B = Type.Object({ x: Type.Literal('B'), y: Type.String() }) + const C = Type.Object({ x: Type.Literal('C') }) + const D = Type.Object({ x: Type.Literal('D') }) + const T = Type.Intersect([A, B, C, D]) + const I = Type.Index(T, ['x', 'y']) + Assert.IsTrue(KindGuard.IsUnion(I)) + Assert.IsTrue(KindGuard.IsIntersect(I.anyOf[0])) + Assert.IsTrue(KindGuard.IsIntersect(I.anyOf[1])) + Assert.IsTrue(KindGuard.IsNumber(I.anyOf[1].allOf[0])) + Assert.IsTrue(KindGuard.IsString(I.anyOf[1].allOf[1])) + }) + it('Should Index 26', () => { + const T = Type.Recursive((This) => + Type.Object({ + x: Type.String(), + y: Type.Number(), + z: This, + }), + ) + const I = Type.Index(T, ['x', 'y', 'z']) + Assert.IsTrue(KindGuard.IsUnion(I)) + Assert.IsTrue(KindGuard.IsString(I.anyOf[0])) + Assert.IsTrue(KindGuard.IsNumber(I.anyOf[1])) + Assert.IsTrue(KindGuard.IsThis(I.anyOf[2])) + }) + it('Should Index 27', () => { + const T = Type.Object({ + 0: Type.String(), + 1: Type.Number(), + }) + const I = Type.Index(T, [0, 1]) + Assert.IsTrue(KindGuard.IsUnion(I)) + Assert.IsTrue(KindGuard.IsString(I.anyOf[0])) + Assert.IsTrue(KindGuard.IsNumber(I.anyOf[1])) + }) + it('Should Index 28', () => { + const T = Type.Object({ + 0: Type.String(), + '1': Type.Number(), + }) + const I = Type.Index(T, [0, '1']) + Assert.IsTrue(KindGuard.IsUnion(I)) + Assert.IsTrue(KindGuard.IsString(I.anyOf[0])) + Assert.IsTrue(KindGuard.IsNumber(I.anyOf[1])) + }) + it('Should Index 29', () => { + const T = Type.Object({ + 0: Type.String(), + '1': Type.Number(), + }) + const I = Type.Index(T, Type.Union([Type.Literal(0), Type.Literal('1')])) + Assert.IsTrue(KindGuard.IsUnion(I)) + Assert.IsTrue(KindGuard.IsString(I.anyOf[0])) + Assert.IsTrue(KindGuard.IsNumber(I.anyOf[1])) + }) + it('Should Index 30', () => { + const T = Type.Object({ + 0: Type.String(), + '1': Type.Number(), + }) + const I = Type.Index(T, Type.Union([Type.Literal(0), Type.Literal(1)])) + Assert.IsTrue(KindGuard.IsUnion(I)) + Assert.IsTrue(KindGuard.IsString(I.anyOf[0])) + Assert.IsTrue(KindGuard.IsNumber(I.anyOf[1])) + // Note: Expect TNever for anyOf[1] but permit for TNumber due to IndexedAccess + // Resolve() which currently cannot differentiate between string and numeric keys + // on the object. This may be resolvable in later revisions, but test for this + // fall-through to ensure case is documented. For review. + }) + // -------------------------------------------------------- + // Modifier Optional Indexing + // -------------------------------------------------------- + it('Should Index 31', () => { + const T = Type.Object({ + x: Type.Optional(Type.String()), + y: Type.Number(), + }) + const I = Type.Index(T, ['x']) + Assert.IsTrue(KindGuard.IsOptional(I)) + Assert.IsTrue(KindGuard.IsString(I)) + }) + it('Should Index 32', () => { + const T = Type.Object({ + x: Type.Optional(Type.String()), + y: Type.Number(), + }) + const I = Type.Index(T, ['y']) + Assert.IsFalse(KindGuard.IsOptional(I)) + Assert.IsTrue(KindGuard.IsNumber(I)) + }) + it('Should Index 33', () => { + const T = Type.Object({ + x: Type.Optional(Type.String()), + y: Type.Number(), + }) + const I = Type.Index(T, ['x', 'y']) + Assert.IsTrue(KindGuard.IsOptional(I)) + Assert.IsTrue(KindGuard.IsUnion(I)) + Assert.IsTrue(KindGuard.IsString(I.anyOf[0])) + Assert.IsTrue(KindGuard.IsNumber(I.anyOf[1])) + }) + it('Should Index 34', () => { + const T = Type.String() + const I = Type.Index(T, ['x']) + Assert.IsTrue(KindGuard.IsNever(I)) + }) + it('Should Index 35', () => { + const T = Type.Array(Type.String()) + const I = Type.Index(T, Type.Number()) + Assert.IsTrue(KindGuard.IsString(I)) + }) + it('Should Index 36', () => { + const T = Type.Array(Type.String()) + const I = Type.Index(T, ['[number]']) + Assert.IsTrue(KindGuard.IsString(I)) + }) +}) diff --git a/test/runtime/type/guard/kind/integer.ts b/test/runtime/type/guard/kind/integer.ts new file mode 100644 index 000000000..a8fce9a9d --- /dev/null +++ b/test/runtime/type/guard/kind/integer.ts @@ -0,0 +1,14 @@ +import { KindGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TInteger', () => { + it('Should guard for TInteger', () => { + const R = KindGuard.IsInteger(Type.Integer()) + Assert.IsTrue(R) + }) + it('Should not guard for TInteger', () => { + const R = KindGuard.IsInteger(null) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/guard/kind/intersect.ts b/test/runtime/type/guard/kind/intersect.ts new file mode 100644 index 000000000..f1678103f --- /dev/null +++ b/test/runtime/type/guard/kind/intersect.ts @@ -0,0 +1,39 @@ +import { KindGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TIntersect', () => { + it('Should guard for TIntersect', () => { + const R = KindGuard.IsIntersect( + Type.Intersect([ + Type.Object({ + x: Type.Number(), + }), + Type.Object({ + y: Type.Number(), + }), + ]), + ) + Assert.IsTrue(R) + }) + it('Should not guard for TIntersect', () => { + const R = KindGuard.IsIntersect( + Type.Union([ + Type.Object({ + x: Type.Number(), + }), + Type.Object({ + y: Type.Number(), + }), + ]), + ) + Assert.IsFalse(R) + }) + it('Should throw for intersected transform types', () => { + const N = Type.Transform(Type.Number()) + .Decode((value) => value) + .Encode((value) => value) + + Assert.Throws(() => Type.Intersect([N, N])) + }) +}) diff --git a/test/runtime/type/guard/kind/iterator.ts b/test/runtime/type/guard/kind/iterator.ts new file mode 100644 index 000000000..5e650b41c --- /dev/null +++ b/test/runtime/type/guard/kind/iterator.ts @@ -0,0 +1,16 @@ +import { KindGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TIterator', () => { + it('Should guard for TIterator', () => { + const T = Type.Iterator(Type.Any()) + const R = KindGuard.IsIterator(T) + Assert.IsTrue(R) + }) + it('Should not guard for TIterator', () => { + const T = null + const R = KindGuard.IsIterator(T) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/guard/kind/keyof.ts b/test/runtime/type/guard/kind/keyof.ts new file mode 100644 index 000000000..5d2867b3a --- /dev/null +++ b/test/runtime/type/guard/kind/keyof.ts @@ -0,0 +1,76 @@ +import { KindGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TKeyOf', () => { + it('Should KeyOf 1', () => { + const T = Type.Object({ + x: Type.Number(), + y: Type.Number(), + }) + const K = Type.KeyOf(T) + Assert.IsTrue(KindGuard.IsUnion(K)) + Assert.IsTrue(KindGuard.IsLiteral(K.anyOf[0])) + Assert.IsTrue(KindGuard.IsLiteral(K.anyOf[1])) + }) + it('Should KeyOf 2', () => { + const T = Type.Recursive((Self) => + Type.Object({ + x: Type.Number(), + y: Type.Array(Self), + }), + ) + const K = Type.KeyOf(T) + Assert.IsTrue(KindGuard.IsUnion(K)) + Assert.IsTrue(KindGuard.IsLiteral(K.anyOf[0])) + Assert.IsTrue(KindGuard.IsLiteral(K.anyOf[1])) + }) + it('Should KeyOf 3', () => { + const T = Type.Intersect([ + Type.Object({ + x: Type.Number(), + }), + Type.Object({ + y: Type.Number(), + }), + ]) + const K = Type.KeyOf(T) + Assert.IsTrue(KindGuard.IsUnion(K)) + Assert.IsTrue(KindGuard.IsLiteral(K.anyOf[0])) + Assert.IsTrue(KindGuard.IsLiteral(K.anyOf[1])) + }) + it('Should KeyOf 4', () => { + const T = Type.Union([ + Type.Object({ + x: Type.Number(), + }), + Type.Object({ + y: Type.Number(), + }), + ]) + const K = Type.KeyOf(T) + Assert.IsTrue(KindGuard.IsNever(K)) + }) + it('Should KeyOf 5', () => { + const T = Type.Null() + const K = Type.KeyOf(T) + Assert.IsTrue(KindGuard.IsNever(K)) + }) + it('Should KeyOf 6', () => { + const T = Type.Array(Type.Number()) + const K = Type.KeyOf(T) + Assert.IsTrue(KindGuard.IsNumber(K)) + }) + it('Should KeyOf 7', () => { + const T = Type.Tuple([]) + const K = Type.KeyOf(T) + Assert.IsTrue(KindGuard.IsNever(K)) + }) + it('Should KeyOf 8', () => { + const T = Type.Tuple([Type.Number(), Type.Null()]) + const K = Type.KeyOf(T) + Assert.IsTrue(KindGuard.IsUnion(K)) + Assert.IsEqual(K.anyOf[0].const, '0') + Assert.IsEqual(K.anyOf[1].const, '1') + }) +}) diff --git a/test/runtime/type/guard/kind/kind.ts b/test/runtime/type/guard/kind/kind.ts new file mode 100644 index 000000000..6ae707a6d --- /dev/null +++ b/test/runtime/type/guard/kind/kind.ts @@ -0,0 +1,13 @@ +import { KindGuard, Kind } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TKind', () => { + it('Should guard 1', () => { + const T = { [Kind]: 'Kind' } + Assert.IsTrue(KindGuard.IsKind(T)) + }) + it('Should guard 2', () => { + const T = {} + Assert.IsFalse(KindGuard.IsKind(T)) + }) +}) diff --git a/test/runtime/type/guard/kind/literal.ts b/test/runtime/type/guard/kind/literal.ts new file mode 100644 index 000000000..413bc94b0 --- /dev/null +++ b/test/runtime/type/guard/kind/literal.ts @@ -0,0 +1,18 @@ +import { KindGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TLiteral', () => { + it('Should guard for TLiteral of String', () => { + const R = KindGuard.IsLiteral(Type.Literal('hello')) + Assert.IsTrue(R) + }) + it('Should guard for TLiteral of Number', () => { + const R = KindGuard.IsLiteral(Type.Literal(42)) + Assert.IsTrue(R) + }) + it('Should guard for TLiteral of Boolean', () => { + const R = KindGuard.IsLiteral(Type.Literal(true)) + Assert.IsTrue(R) + }) +}) diff --git a/test/runtime/type/guard/kind/lowercase.ts b/test/runtime/type/guard/kind/lowercase.ts new file mode 100644 index 000000000..d2ef0ccee --- /dev/null +++ b/test/runtime/type/guard/kind/lowercase.ts @@ -0,0 +1,33 @@ +import { KindGuard, Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/Lowercase', () => { + it('Should guard for Lowercase 1', () => { + const T = Type.Lowercase(Type.Literal('HELLO'), { $id: 'hello', foo: 1 }) + Assert.IsTrue(KindGuard.IsLiteral(T)) + Assert.IsEqual(T.const, 'hello') + Assert.IsEqual(T.$id, 'hello') + Assert.IsEqual(T.foo, 1) + }) + it('Should guard for Lowercase 2', () => { + const T = Type.Lowercase(Type.Literal('HELLO')) + Assert.IsTrue(KindGuard.IsLiteral(T)) + Assert.IsEqual(T.const, 'hello') + }) + it('Should guard for Lowercase 3', () => { + const T = Type.Lowercase(Type.Union([Type.Literal('HELLO'), Type.Literal('WORLD')])) + Assert.IsTrue(KindGuard.IsUnion(T)) + Assert.IsEqual(T.anyOf[0].const, 'hello') + Assert.IsEqual(T.anyOf[1].const, 'world') + }) + it('Should guard for Lowercase 4', () => { + const T = Type.Lowercase(Type.TemplateLiteral('HELLO${0|1}')) + Assert.IsTrue(KindGuard.IsTemplateLiteral(T)) + Assert.IsEqual(T.pattern, '^(hello0|hello1)$') + }) + it('Should guard for Lowercase 5', () => { + const T = Type.Lowercase(Type.TemplateLiteral([Type.Literal('HELLO'), Type.Union([Type.Literal(0), Type.Literal(1)])])) + Assert.IsTrue(KindGuard.IsTemplateLiteral(T)) + Assert.IsEqual(T.pattern, '^(hello0|hello1)$') + }) +}) diff --git a/test/runtime/type/guard/kind/mapped.ts b/test/runtime/type/guard/kind/mapped.ts new file mode 100644 index 000000000..c4673e126 --- /dev/null +++ b/test/runtime/type/guard/kind/mapped.ts @@ -0,0 +1,609 @@ +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +// prettier-ignore +describe('guard/kind/Mapped', () => { + it('Should guard mapped 1', () => { + const T = Type.Mapped(Type.Union([ + Type.Literal('x'), + Type.Literal('y'), + Type.Literal('z'), + ]), _ => Type.Number(), { custom: 1 }) + Assert.IsEqual(T, Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number() + }, { custom: 1 })) + }) + it('Should guard mapped 2', () => { + const T = Type.Mapped(Type.Union([ + Type.Literal('x'), + Type.Literal('y'), + Type.Literal('z'), + ]), _ => Type.Number()) + Assert.IsEqual(T, Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number() + })) + }) + it('Should guard mapped 3', () => { + const T = Type.Mapped(Type.Union([ + Type.Literal('x'), + Type.Literal('y'), + Type.Literal('z'), + ]), K => K) + Assert.IsEqual(T, Type.Object({ + x: Type.Literal('x'), + y: Type.Literal('y'), + z: Type.Literal('z'), + })) + }) + it('Should guard mapped 4', () => { + const T = Type.Mapped(Type.TemplateLiteral('${0|1}${0|1}'), K => Type.Number()) + Assert.IsEqual(T, Type.Object({ + '00': Type.Number(), + '01': Type.Number(), + '10': Type.Number(), + '11': Type.Number(), + })) + }) + it('Should guard mapped 5', () => { + const T = Type.Mapped(Type.TemplateLiteral('${a|b}'), X => + Type.Mapped(Type.TemplateLiteral('${c|d}'), Y => + Type.Mapped(Type.TemplateLiteral('${e|f}'), Z => + Type.Tuple([X, Y, Z]) + ) + ) + ) + Assert.IsEqual(T, Type.Object({ + a: Type.Object({ + c: Type.Object({ + e: Type.Tuple([Type.Literal("a"), Type.Literal("c"), Type.Literal("e")]), + f: Type.Tuple([Type.Literal("a"), Type.Literal("c"), Type.Literal("f")]) + }), + d: Type.Object({ + e: Type.Tuple([Type.Literal("a"), Type.Literal("d"), Type.Literal("e")]), + f: Type.Tuple([Type.Literal("a"), Type.Literal("d"), Type.Literal("f")]) + }), + }), + b: Type.Object({ + c: Type.Object({ + e: Type.Tuple([Type.Literal("b"), Type.Literal("c"), Type.Literal("e")]), + f: Type.Tuple([Type.Literal("b"), Type.Literal("c"), Type.Literal("f")]) + }), + d: Type.Object({ + e: Type.Tuple([Type.Literal("b"), Type.Literal("d"), Type.Literal("e")]), + f: Type.Tuple([Type.Literal("b"), Type.Literal("d"), Type.Literal("f")]) + }), + }), + })) + }) + it('Should guard mapped 6', () => { + const T = Type.Object({ + x: Type.Number(), + y: Type.String(), + z: Type.Boolean() + }) + const M = Type.Mapped(Type.KeyOf(T), K => K) + Assert.IsEqual(M, Type.Object({ + x: Type.Literal('x'), + y: Type.Literal('y'), + z: Type.Literal('z') + })) + }) + it('Should guard mapped 7', () => { + const T = Type.Object({ + x: Type.Number(), + y: Type.String(), + z: Type.Boolean() + }) + const M = Type.Mapped(Type.KeyOf(T), K => Type.Index(T, K)) + Assert.IsEqual(M, Type.Object({ + x: Type.Number(), + y: Type.String(), + z: Type.Boolean() + })) + }) + it('Should guard mapped 8', () => { + const T = Type.Object({ + x: Type.Number(), + y: Type.String(), + z: Type.Boolean() + }) + const M = Type.Mapped(Type.KeyOf(T), K => Type.Index(T, K, { custom: 1 })) + Assert.IsEqual(M, Type.Object({ + x: Type.Number({ custom: 1 }), + y: Type.String({ custom: 1 }), + z: Type.Boolean({ custom: 1 }) + })) + }) + // ---------------------------------------------------------------- + // Extract + // ---------------------------------------------------------------- + it('Should guard mapped 9', () => { + const T = Type.Object({ + x: Type.Union([Type.String(), Type.Number(), Type.Boolean()]) + }) + const M = Type.Mapped(Type.KeyOf(T), K => { + return Type.Extract(Type.Index(T, K), Type.String()) + }) + Assert.IsEqual(M, Type.Object({ + x: Type.String() + })) + }) + it('Should guard mapped 10', () => { + const T = Type.Object({ + x: Type.Union([Type.String(), Type.Number(), Type.Boolean()]) + }) + const M = Type.Mapped(Type.KeyOf(T), K => { + return Type.Extract(Type.Index(T, K), Type.Union([ + Type.String(), + Type.Number() + ])) + }) + Assert.IsEqual(M, Type.Object({ + x: Type.Union([Type.String(), Type.Number()]) + })) + }) + it('Should guard mapped 11', () => { + const T = Type.Object({ + x: Type.Union([Type.String(), Type.Number(), Type.Boolean()]) + }) + const M = Type.Mapped(Type.KeyOf(T), K => { + return Type.Extract(Type.Index(T, K), Type.Null()) + }) + Assert.IsEqual(M, Type.Object({ + x: Type.Never() + })) + }) + // ---------------------------------------------------------------- + // Extends + // ---------------------------------------------------------------- + it('Should guard mapped 12', () => { + const T = Type.Object({ + x: Type.Number(), + y: Type.String(), + z: Type.Boolean() + }) + const M = Type.Mapped(Type.KeyOf(T), K => { + return ( + Type.Extends(K, Type.Literal('x'), Type.Literal(1), + Type.Extends(K, Type.Literal('y'), Type.Literal(2), + Type.Extends(K, Type.Literal('z'), Type.Literal(3), Type.Never()))) + ) + }) + Assert.IsEqual(M, Type.Object({ + x: Type.Literal(1), + y: Type.Literal(2), + z: Type.Literal(3), + })) + }) + it('Should guard mapped 13', () => { + const T = Type.Object({ + x: Type.Number(), + y: Type.String(), + z: Type.Boolean() + }) + const M = Type.Mapped(Type.KeyOf(T), K => { + return ( + Type.Extends(Type.Index(T, K), Type.Number(), Type.Literal(3), + Type.Extends(Type.Index(T, K), Type.String(), Type.Literal(2), + Type.Extends(Type.Index(T, K), Type.Boolean(), Type.Literal(1), Type.Never()))) + ) + }) + Assert.IsEqual(M, Type.Object({ + x: Type.Literal(3), + y: Type.Literal(2), + z: Type.Literal(1), + })) + }) + // ---------------------------------------------------------------- + // Exclude + // ---------------------------------------------------------------- + it('Should guard mapped 14', () => { + const T = Type.Object({ + x: Type.Union([Type.String(), Type.Number(), Type.Boolean()]) + }) + const M = Type.Mapped(Type.KeyOf(T), K => { + return Type.Exclude(Type.Index(T, K), Type.String()) + }) + Assert.IsEqual(M, Type.Object({ + x: Type.Union([Type.Number(), Type.Boolean()]) + })) + }) + it('Should guard mapped 15', () => { + const T = Type.Object({ + x: Type.Union([Type.String(), Type.Number(), Type.Boolean()]) + }) + const M = Type.Mapped(Type.KeyOf(T), K => { + return Type.Exclude(Type.Index(T, K), Type.Union([ + Type.String(), + Type.Number() + ])) + }) + Assert.IsEqual(M, Type.Object({ + x: Type.Boolean() + })) + }) + it('Should guard mapped 16', () => { + const T = Type.Object({ + x: Type.Union([Type.String(), Type.Number(), Type.Boolean()]) + }) + const M = Type.Mapped(Type.KeyOf(T), K => { + return Type.Exclude(Type.Index(T, K), Type.Null()) + }) + Assert.IsEqual(M, Type.Object({ + x: Type.Union([Type.String(), Type.Number(), Type.Boolean()]) + })) + }) + // ---------------------------------------------------------------- + // Non-Evaluated + // ---------------------------------------------------------------- + it('Should guard mapped 17', () => { + const T = Type.Object({ x: Type.Number() }) + const M = Type.Mapped(Type.KeyOf(T), K => Type.Array(Type.Index(T, K))) + Assert.IsEqual(M, Type.Object({ x: Type.Array(Type.Number()) })) + }) + it('Should guard mapped 18', () => { + const T = Type.Object({ x: Type.Number() }) + const M = Type.Mapped(Type.KeyOf(T), K => Type.Promise(Type.Index(T, K))) + Assert.IsEqual(M, Type.Object({ x: Type.Promise(Type.Number()) })) + }) + it('Should guard mapped 19', () => { + const T = Type.Object({ x: Type.Number() }) + const M = Type.Mapped(Type.KeyOf(T), K => Type.Function([Type.Index(T, K)], Type.Index(T, K))) + Assert.IsEqual(M, Type.Object({ x: Type.Function([Type.Number()], Type.Number())})) + }) + it('Should guard mapped 20', () => { + const T = Type.Object({ x: Type.Number() }) + const M = Type.Mapped(Type.KeyOf(T), K => Type.Tuple([Type.Index(T, K), Type.Index(T, K)])) + Assert.IsEqual(M, Type.Object({ x: Type.Tuple([Type.Number(), Type.Number()]) })) + }) + it('Should guard mapped 21', () => { + const T = Type.Object({ x: Type.Number() }) + const M = Type.Mapped(Type.KeyOf(T), K => Type.Union([Type.Index(T, K), Type.Index(T, K)])) + Assert.IsEqual(M, Type.Object({ x: Type.Union([Type.Number(), Type.Number()]) })) + }) + it('Should guard mapped 22', () => { + const T = Type.Object({ x: Type.Number() }) + const M = Type.Mapped(Type.KeyOf(T), K => Type.Intersect([Type.Index(T, K), Type.Index(T, K)])) + Assert.IsEqual(M, Type.Object({ x: Type.Intersect([Type.Number(), Type.Number()]) })) + }) + // ---------------------------------------------------------------- + // Numeric Keys + // ---------------------------------------------------------------- + it('Should guard mapped 23', () => { + const T = Type.Object({ + 0: Type.Number(), + 1: Type.Number(), + 2: Type.Number() + }) + const M = Type.Mapped(Type.KeyOf(T), K => K) + Assert.IsEqual(M, Type.Object({ + 0: Type.Literal('0'), + 1: Type.Literal('1'), + 2: Type.Literal('2'), + })) + }) + it('Should guard mapped 24', () => { + const T = Type.Object({ + 0: Type.Number(), + 1: Type.Number(), + 2: Type.Number() + }) + const M = Type.Mapped(Type.KeyOf(T), K => Type.Index(T, K)) + Assert.IsEqual(M, Type.Object({ + 0: Type.Number(), + 1: Type.Number(), + 2: Type.Number(), + })) + }) + it('Should guard mapped 25', () => { + const T = Type.Object({ + 0: Type.Number(), + 1: Type.Number(), + 2: Type.Number() + }) + const M = Type.Mapped(Type.KeyOf(T), K => Type.String()) + Assert.IsEqual(M, Type.Object({ + 0: Type.String(), + 1: Type.String(), + 2: Type.String(), + })) + }) + it('Should guard mapped 26', () => { + const T = Type.Object({ + 0: Type.Number(), + 1: Type.Number(), + 2: Type.Number() + }) + const M = Type.Mapped(Type.KeyOf(T), K => Type.Extends(K, Type.Literal('1'), Type.String(), Type.Number())) + Assert.IsEqual(M, Type.Object({ + 0: Type.Number(), + 1: Type.String(), + 2: Type.Number(), + })) + }) + // ---------------------------------------------------------------- + // Modifiers: Optional + // ---------------------------------------------------------------- + it('Should guard mapped 27', () => { + const T = Type.Object({ + x: Type.Optional(Type.Number()), + y: Type.Number() + }) + // subtractive + const M = Type.Mapped(Type.KeyOf(T), K => Type.Optional(Type.Index(T, K), false)) + Assert.IsEqual(M, Type.Object({ + x: Type.Number(), + y: Type.Number() + })) + }) + it('Should guard mapped 28', () => { + const T = Type.Object({ + x: Type.Optional(Type.Number()), + y: Type.Number() + }) + // additive + const M = Type.Mapped(Type.KeyOf(T), K => Type.Optional(Type.Index(T, K), true)) + Assert.IsEqual(M, Type.Object({ + x: Type.Optional(Type.Number()), + y: Type.Optional(Type.Number()) + })) + }) + // ---------------------------------------------------------------- + // Modifiers: Readonly + // ---------------------------------------------------------------- + it('Should guard mapped 27', () => { + const T = Type.Object({ + x: Type.Readonly(Type.Number()), + y: Type.Number() + }) + // subtractive + const M = Type.Mapped(Type.KeyOf(T), K => Type.Readonly(Type.Index(T, K), false)) + Assert.IsEqual(M, Type.Object({ + x: Type.Number(), + y: Type.Number() + })) + }) + it('Should guard mapped 28', () => { + const T = Type.Object({ + x: Type.Readonly(Type.Number()), + y: Type.Number() + }) + // additive + const M = Type.Mapped(Type.KeyOf(T), K => Type.Readonly(Type.Index(T, K), true)) + Assert.IsEqual(M, Type.Object({ + x: Type.Readonly(Type.Number()), + y: Type.Readonly(Type.Number()) + })) + }) + // ---------------------------------------------------------------- + // Finite Boolean + // ---------------------------------------------------------------- + it('Should guard mapped 29', () => { + const T = Type.TemplateLiteral('${boolean}') + const M = Type.Mapped(T, K => K) + Assert.IsEqual(M, Type.Object({ + true: Type.Literal('true'), + false: Type.Literal('false'), + })) + }) + it('Should guard mapped 30', () => { + const T = Type.TemplateLiteral('${0|1}${boolean}') + const M = Type.Mapped(T, K => K) + Assert.IsEqual(M, Type.Object({ + '0true': Type.Literal('0true'), + '0false': Type.Literal('0false'), + '1true': Type.Literal('1true'), + '1false': Type.Literal('1false'), + })) + }) + it('Should guard mapped 31', () => { + const T = Type.TemplateLiteral('${boolean}${0|1}') + const M = Type.Mapped(T, K => K) + Assert.IsEqual(M, Type.Object({ + 'true0': Type.Literal('true0'), + 'true1': Type.Literal('true1'), + 'false0': Type.Literal('false0'), + 'false1': Type.Literal('false1'), + })) + }) + // ---------------------------------------------------------------- + // Numeric Mapping + // ---------------------------------------------------------------- + it('Should guard mapped 32', () => { + const T = Type.TemplateLiteral([ + Type.Union([Type.Literal(0), Type.Literal(1)]), + Type.Union([Type.Literal(0), Type.Literal(1)]), + ]) + const M = Type.Mapped(T, (K) => K) + Assert.IsEqual(M, Type.Object({ + '00': Type.Literal('00'), + '01': Type.Literal('01'), + '10': Type.Literal('10'), + '11': Type.Literal('11'), + })) + }) + // ---------------------------------------------------------------- + // Indexed Key Remap + // ---------------------------------------------------------------- + it('Should guard mapped 33', () => { + const T = Type.Object({ + hello: Type.Number(), + world: Type.String(), + }) + const M = Type.Mapped(Type.Uppercase(Type.KeyOf(T)), (K) => { + return Type.Index(T, Type.Lowercase(K)) + }) + Assert.IsEqual(M, Type.Object({ + HELLO: Type.Number(), + WORLD: Type.String() + })) + }) + // ---------------------------------------------------------------- + // Partial + // ---------------------------------------------------------------- + it('Should guard mapped 34', () => { + const T = Type.Object({ + x: Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + y: Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + }) + const M = Type.Mapped(Type.KeyOf(T), (K) => { + return Type.Partial(Type.Index(T, K)) + }) + Assert.IsEqual(M, Type.Object({ + x: Type.Partial(Type.Object({ + x: Type.Optional(Type.Number()), + y: Type.Optional(Type.Number()), + })), + y: Type.Partial(Type.Object({ + x: Type.Optional(Type.Number()), + y: Type.Optional(Type.Number()), + })), + })) + }) + // ---------------------------------------------------------------- + // Required + // ---------------------------------------------------------------- + it('Should guard mapped 35', () => { + const T = Type.Object({ + x: Type.Partial(Type.Object({ + x: Type.Number(), + y: Type.Number(), + })), + y: Type.Partial(Type.Object({ + x: Type.Number(), + y: Type.Number(), + })), + }) + const M = Type.Mapped(Type.KeyOf(T), (K) => { + return Type.Required(Type.Index(T, K)) + }) + Assert.IsEqual(M, Type.Object({ + x: Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + y: Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + })) + }) + // ------------------------------------------------------------------ + // Pick With Key + // ------------------------------------------------------------------ + it('Should guard mapped 36', () => { + const T = Type.Object({ + x: Type.Object({ + x: Type.Number(), + y: Type.Number() + }), + y: Type.Object({ + x: Type.Number(), + y: Type.Number() + }) + }) + const M = Type.Mapped(Type.KeyOf(T), K => { + return Type.Pick(T, K) + }) + Assert.IsEqual(M, Type.Object({ + x: Type.Object({ + x: Type.Object({ + x: Type.Number(), + y: Type.Number(), + }) + }), + y: Type.Object({ + y: Type.Object({ + x: Type.Number(), + y: Type.Number(), + }) + }), + })) + }) + // ------------------------------------------------------------------ + // Pick With Result + // ------------------------------------------------------------------ + it('Should guard mapped 37', () => { + const T = Type.Object({ + x: Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + y: Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + }) + const M = Type.Mapped(Type.KeyOf(T), (K) => { + return Type.Pick(Type.Index(T, K), ['x']) + }) + Assert.IsEqual(M, Type.Object({ + x: Type.Object({ x: Type.Number() }), + y: Type.Object({ x: Type.Number() }) + })) + }) + // ------------------------------------------------------------------ + // Omit With Key + // ------------------------------------------------------------------ + it('Should guard mapped 36', () => { + const T = Type.Object({ + x: Type.Object({ + x: Type.Number(), + y: Type.Number() + }), + y: Type.Object({ + x: Type.Number(), + y: Type.Number() + }) + }) + const M = Type.Mapped(Type.KeyOf(T), K => { + return Type.Omit(T, K) + }) + Assert.IsEqual(M, Type.Object({ + x: Type.Object({ + y: Type.Object({ + x: Type.Number(), + y: Type.Number(), + }) + }), + y: Type.Object({ + x: Type.Object({ + x: Type.Number(), + y: Type.Number(), + }) + }), + })) + }) + // ------------------------------------------------------------------ + // Omit With Result + // ------------------------------------------------------------------ + it('Should guard mapped 37', () => { + const T = Type.Object({ + x: Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + y: Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + }) + const M = Type.Mapped(Type.KeyOf(T), (K) => { + return Type.Omit(Type.Index(T, K), ['x']) + }) + Assert.IsEqual(M, Type.Object({ + x: Type.Object({ y: Type.Number() }), + y: Type.Object({ y: Type.Number() }) + })) + }) +}) diff --git a/test/runtime/type/guard/kind/not.ts b/test/runtime/type/guard/kind/not.ts new file mode 100644 index 000000000..a30c2e6dd --- /dev/null +++ b/test/runtime/type/guard/kind/not.ts @@ -0,0 +1,10 @@ +import { KindGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TNot', () => { + it('Should guard for TNot', () => { + const R = KindGuard.IsNot(Type.Not(Type.String())) + Assert.IsTrue(R) + }) +}) diff --git a/test/runtime/type/guard/kind/null.ts b/test/runtime/type/guard/kind/null.ts new file mode 100644 index 000000000..81e29b0f3 --- /dev/null +++ b/test/runtime/type/guard/kind/null.ts @@ -0,0 +1,14 @@ +import { KindGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TNull', () => { + it('Should guard for TNull', () => { + const R = KindGuard.IsNull(Type.Null()) + Assert.IsTrue(R) + }) + it('Should not guard for TNull', () => { + const R = KindGuard.IsNull(null) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/guard/kind/number.ts b/test/runtime/type/guard/kind/number.ts new file mode 100644 index 000000000..00a992ba1 --- /dev/null +++ b/test/runtime/type/guard/kind/number.ts @@ -0,0 +1,14 @@ +import { KindGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TNumber', () => { + it('Should guard for TNumber', () => { + const R = KindGuard.IsNumber(Type.Number()) + Assert.IsTrue(R) + }) + it('Should not guard for TNumber', () => { + const R = KindGuard.IsNumber(null) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/guard/kind/object.ts b/test/runtime/type/guard/kind/object.ts new file mode 100644 index 000000000..f0c45189d --- /dev/null +++ b/test/runtime/type/guard/kind/object.ts @@ -0,0 +1,19 @@ +import { KindGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TObject', () => { + it('Should guard for TObject', () => { + const R = KindGuard.IsObject( + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + ) + Assert.IsTrue(R) + }) + it('Should not guard for TObject', () => { + const R = KindGuard.IsObject(null) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/guard/kind/omit.ts b/test/runtime/type/guard/kind/omit.ts new file mode 100644 index 000000000..1aba2a82d --- /dev/null +++ b/test/runtime/type/guard/kind/omit.ts @@ -0,0 +1,129 @@ +import { KindGuard, Type, Kind, TransformKind } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TOmit', () => { + // ------------------------------------------------------------------------- + // case: https://github.com/sinclairzx81/typebox/issues/384 + // ------------------------------------------------------------------------- + it('Should support TUnsafe omit properties with no Kind', () => { + const T = Type.Omit(Type.Object({ x: Type.Unsafe({ x: 1 }), y: Type.Number() }), ['x']) + Assert.IsEqual(T.required, ['y']) + }) + it('Should support TUnsafe omit properties with unregistered Kind', () => { + const T = Type.Omit(Type.Object({ x: Type.Unsafe({ x: 1, [Kind]: 'UnknownOmitType' }), y: Type.Number() }), ['x']) + Assert.IsEqual(T.required, ['y']) + }) + // ------------------------------------------------------------------------- + // Standard Tests + // ------------------------------------------------------------------------- + it('Should Omit 1', () => { + const T = Type.Omit( + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + ['x'], + ) + Assert.IsTrue(KindGuard.IsNumber(T.properties.y)) + Assert.IsEqual(T.required, ['y']) + }) + it('Should Omit 2', () => { + const T = Type.Omit( + Type.Object({ + x: Type.Number(), + y: Type.Optional(Type.Number()), + }), + ['x'], + ) + Assert.IsTrue(KindGuard.IsNumber(T.properties.y)) + Assert.IsEqual(T.required, undefined) + }) + it('Should Omit 3', () => { + const L = Type.Literal('x') + const T = Type.Omit( + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + L, + ) + Assert.IsTrue(KindGuard.IsNumber(T.properties.y)) + Assert.IsEqual(T.required, ['y']) + }) + it('Should Omit 4', () => { + const L = Type.Literal('x') + const T = Type.Omit(Type.Intersect([Type.Object({ x: Type.Number() }), Type.Object({ y: Type.Number() })]), L) + Assert.IsEqual(KindGuard.IsNumber(T.allOf[1].properties.y), true) + // @ts-ignore + Assert.IsEqual(T.allOf[1].properties.x, undefined) + }) + it('Should Omit 5', () => { + const L = Type.Union([Type.Literal('x'), Type.Literal('y')]) + const T = Type.Omit( + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + L, + ) + // @ts-ignore + Assert.IsEqual(T.properties.x, undefined) + // @ts-ignore + Assert.IsEqual(T.properties.y, undefined) + // @ts-ignore + Assert.IsEqual(T.required, undefined) + }) + it('Should Omit 6', () => { + const L = Type.Union([Type.Literal('x'), Type.Literal('y'), Type.Literal('z')]) + const T = Type.Omit( + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + L, + ) + // @ts-ignore + Assert.IsEqual(T.properties.x, undefined) + // @ts-ignore + Assert.IsEqual(T.properties.y, undefined) + // @ts-ignore + Assert.IsEqual(T.required, undefined) + }) + it('Should Omit 7', () => { + const L = Type.TemplateLiteral([Type.Literal('a'), Type.Union([Type.Literal('b'), Type.Literal('c')])]) + const T = Type.Omit( + Type.Object({ + ab: Type.Number(), + ac: Type.Number(), + ad: Type.Number(), + }), + L, + ) + Assert.IsTrue(KindGuard.IsNumber(T.properties.ad)) + Assert.IsEqual(T.required, ['ad']) + }) + // ---------------------------------------------------------------- + // Discard + // ---------------------------------------------------------------- + it('Should override $id', () => { + const A = Type.Object({ x: Type.Number() }, { $id: 'A' }) + const T = Type.Omit(A, ['x'], { $id: 'T' }) + Assert.IsEqual(T.$id!, 'T') + }) + it('Should discard $id', () => { + const A = Type.Object({ x: Type.Number() }, { $id: 'A' }) + const T = Type.Omit(A, ['x']) + Assert.IsFalse('$id' in T) + }) + it('Should discard transform', () => { + const T = Type.Object({ + x: Type.Number(), + y: Type.String(), + }) + const S = Type.Transform(T) + .Decode((value) => value) + .Encode((value) => value) + const R = Type.Omit(S, ['x']) + Assert.IsFalse(TransformKind in R) + }) +}) diff --git a/test/runtime/type/guard/kind/partial.ts b/test/runtime/type/guard/kind/partial.ts new file mode 100644 index 000000000..f9f4615d4 --- /dev/null +++ b/test/runtime/type/guard/kind/partial.ts @@ -0,0 +1,99 @@ +import { KindGuard, TypeRegistry, Type, Kind, TransformKind } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TPartial', () => { + it('Should produce a valid TSchema', () => { + const T = Type.Partial(Type.Object({ x: Type.Number() })) + Assert.IsTrue(KindGuard.IsSchema(T)) + }) + // ------------------------------------------------------------------------- + // case: https://github.com/sinclairzx81/typebox/issues/364 + // ------------------------------------------------------------------------- + it('Should support TUnsafe partial properties with no Kind', () => { + const T = Type.Partial(Type.Object({ x: Type.Unsafe({ x: 1 }) })) + Assert.IsEqual(T.required, undefined) + }) + it('Should support TUnsafe partial properties with unknown Kind', () => { + const T = Type.Partial(Type.Object({ x: Type.Unsafe({ [Kind]: 'UnknownPartialType', x: 1 }) })) + Assert.IsEqual(T.required, undefined) + }) + it('Should support TUnsafe partial properties with known Kind', () => { + TypeRegistry.Set('KnownPartialType', () => true) + const T = Type.Partial(Type.Object({ x: Type.Unsafe({ [Kind]: 'KnownPartialType', x: 1 }) })) + Assert.IsEqual(T.required, undefined) + }) + it('Should support applying partial to intersect', () => { + const A = Type.Object({ x: Type.Number() }) + const B = Type.Object({ y: Type.Number() }) + const I = Type.Intersect([A, B]) + const T = Type.Partial(I) + Assert.IsEqual(T.allOf.length, 2) + Assert.IsEqual(T.allOf[0].required, undefined) + Assert.IsEqual(T.allOf[1].required, undefined) + }) + it('Should support applying partial to union', () => { + const A = Type.Object({ x: Type.Number() }) + const B = Type.Object({ y: Type.Number() }) + const I = Type.Union([A, B]) + const T = Type.Partial(I) + Assert.IsEqual(T.anyOf.length, 2) + Assert.IsEqual(T.anyOf[0].required, undefined) + Assert.IsEqual(T.anyOf[1].required, undefined) + }) + // ---------------------------------------------------------------- + // Discard + // ---------------------------------------------------------------- + it('Should override $id', () => { + const A = Type.Object({ x: Type.Number() }, { $id: 'A' }) + const T = Type.Partial(A, { $id: 'T' }) + Assert.IsEqual(T.$id!, 'T') + }) + it('Should discard $id', () => { + const A = Type.Object({ x: Type.Number() }, { $id: 'A' }) + const T = Type.Partial(A) + Assert.IsFalse('$id' in T) + }) + it('Should discard transform', () => { + const T = Type.Object({ + x: Type.Number(), + y: Type.String(), + }) + const S = Type.Transform(T) + .Decode((value) => value) + .Encode((value) => value) + const R = Type.Partial(S) + Assert.IsFalse(TransformKind in R) + }) + // ------------------------------------------------------------------ + // Intrinsic Passthough + // https://github.com/sinclairzx81/typebox/issues/1169 + // ------------------------------------------------------------------ + it('Should pass through on intrinsic types on union 1', () => { + const T = Type.Partial( + Type.Union([ + Type.Number(), + Type.Object({ + x: Type.Number(), + }), + ]), + ) + Assert.IsTrue(KindGuard.IsUnion(T)) + Assert.IsTrue(KindGuard.IsNumber(T.anyOf[0])) + Assert.IsTrue(KindGuard.IsObject(T.anyOf[1])) + Assert.IsTrue(KindGuard.IsOptional(T.anyOf[1].properties.x)) + }) + it('Should pass through on intrinsic types on union 2', () => { + const T = Type.Partial( + Type.Union([ + Type.Literal(1), + Type.Object({ + x: Type.Number(), + }), + ]), + ) + Assert.IsTrue(KindGuard.IsUnion(T)) + Assert.IsTrue(KindGuard.IsLiteral(T.anyOf[0])) + Assert.IsTrue(KindGuard.IsObject(T.anyOf[1])) + Assert.IsTrue(KindGuard.IsOptional(T.anyOf[1].properties.x)) + }) +}) diff --git a/test/runtime/type/guard/kind/pick.ts b/test/runtime/type/guard/kind/pick.ts new file mode 100644 index 000000000..25bc99e06 --- /dev/null +++ b/test/runtime/type/guard/kind/pick.ts @@ -0,0 +1,131 @@ +import { KindGuard, Type, Kind, TransformKind } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TPick', () => { + // ------------------------------------------------------------------------- + // case: https://github.com/sinclairzx81/typebox/issues/384 + // ------------------------------------------------------------------------- + it('Should support TUnsafe omit properties with no Kind', () => { + const T = Type.Pick( + Type.Object({ + x: Type.Unsafe({ x: 1 }), + y: Type.Number(), + }), + ['x'], + ) + Assert.IsEqual(T.required, ['x']) + }) + it('Should support TUnsafe omit properties with unregistered Kind', () => { + const T = Type.Pick(Type.Object({ x: Type.Unsafe({ x: 1, [Kind]: 'UnknownPickType' }), y: Type.Number() }), ['x']) + Assert.IsEqual(T.required, ['x']) + }) + // ------------------------------------------------------------------------- + // Standard Tests + // ------------------------------------------------------------------------- + it('Should Pick 1', () => { + const T = Type.Pick( + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + ['x'], + ) + Assert.IsTrue(KindGuard.IsNumber(T.properties.x)) + Assert.IsEqual(T.required, ['x']) + }) + it('Should Pick 2', () => { + const T = Type.Pick( + Type.Object({ + x: Type.Optional(Type.Number()), + y: Type.Number(), + }), + ['x'], + ) + Assert.IsTrue(KindGuard.IsNumber(T.properties.x)) + Assert.IsEqual(T.required, undefined) + }) + it('Should Pick 3', () => { + const L = Type.Literal('x') + const T = Type.Pick( + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + L, + ) + Assert.IsTrue(KindGuard.IsNumber(T.properties.x)) + Assert.IsEqual(T.required, ['x']) + }) + it('Should Pick 4', () => { + const L = Type.Literal('x') + const T = Type.Pick(Type.Intersect([Type.Object({ x: Type.Number() }), Type.Object({ y: Type.Number() })]), L) + + Assert.IsTrue(KindGuard.IsNumber(T.allOf[0].properties.x)) + // @ts-ignore + Assert.IsEqual(T.allOf[1].properties.y, undefined) + }) + it('Should Pick 5', () => { + const L = Type.Union([Type.Literal('x'), Type.Literal('y')]) + const T = Type.Pick( + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + L, + ) + Assert.IsTrue(KindGuard.IsNumber(T.properties.x)) + Assert.IsTrue(KindGuard.IsNumber(T.properties.y)) + Assert.IsEqual(T.required, ['x', 'y']) + }) + it('Should Pick 6', () => { + const L = Type.Union([Type.Literal('x'), Type.Literal('y'), Type.Literal('z')]) + const T = Type.Pick( + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + L, + ) + Assert.IsTrue(KindGuard.IsNumber(T.properties.x)) + Assert.IsTrue(KindGuard.IsNumber(T.properties.y)) + Assert.IsEqual(T.required, ['x', 'y']) + }) + it('Should Pick 7', () => { + const L = Type.TemplateLiteral([Type.Literal('a'), Type.Union([Type.Literal('b'), Type.Literal('c')])]) + const T = Type.Pick( + Type.Object({ + ab: Type.Number(), + ac: Type.Number(), + ad: Type.Number(), + }), + L, + ) + Assert.IsTrue(KindGuard.IsNumber(T.properties.ab)) + Assert.IsTrue(KindGuard.IsNumber(T.properties.ac)) + Assert.IsEqual(T.required, ['ab', 'ac']) + }) + // ---------------------------------------------------------------- + // Discard + // ---------------------------------------------------------------- + it('Should override $id', () => { + const A = Type.Object({ x: Type.Number() }, { $id: 'A' }) + const T = Type.Pick(A, ['x'], { $id: 'T' }) + Assert.IsEqual(T.$id!, 'T') + }) + it('Should discard $id', () => { + const A = Type.Object({ x: Type.Number() }, { $id: 'A' }) + const T = Type.Pick(A, ['x']) + Assert.IsFalse('$id' in T) + }) + it('Should discard transform', () => { + const T = Type.Object({ + x: Type.Number(), + y: Type.String(), + }) + const S = Type.Transform(T) + .Decode((value) => value) + .Encode((value) => value) + const R = Type.Pick(S, ['x']) + Assert.IsFalse(TransformKind in R) + }) +}) diff --git a/test/runtime/type/guard/kind/promise.ts b/test/runtime/type/guard/kind/promise.ts new file mode 100644 index 000000000..f59c94625 --- /dev/null +++ b/test/runtime/type/guard/kind/promise.ts @@ -0,0 +1,14 @@ +import { KindGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TPromise', () => { + it('Should guard for TPromise', () => { + const R = KindGuard.IsPromise(Type.Promise(Type.Number())) + Assert.IsTrue(R) + }) + it('Should not guard for TPromise', () => { + const R = KindGuard.IsPromise(null) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/guard/kind/record.ts b/test/runtime/type/guard/kind/record.ts new file mode 100644 index 000000000..85ba4e96e --- /dev/null +++ b/test/runtime/type/guard/kind/record.ts @@ -0,0 +1,176 @@ +import { TypeGuard, PatternNumberExact, PatternStringExact, PatternString, PatternNeverExact } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TRecord', () => { + // ------------------------------------------------------------- + // Overloads + // ------------------------------------------------------------- + it('Should guard overload 1', () => { + const T = Type.Record(Type.Union([Type.Literal('A'), Type.Literal('B')]), Type.String(), { extra: 1 }) + Assert.IsTrue(TypeGuard.IsObject(T)) + Assert.IsTrue(TypeGuard.IsString(T.properties.A)) + Assert.IsTrue(TypeGuard.IsString(T.properties.B)) + Assert.IsEqual(T.extra, 1) + }) + it('Should guard overload 2', () => { + const T = Type.Record(Type.Union([Type.Literal('A')]), Type.String(), { extra: 1 }) // unwrap as literal + Assert.IsTrue(TypeGuard.IsObject(T)) + Assert.IsTrue(TypeGuard.IsString(T.properties.A)) + Assert.IsEqual(T.extra, 1) + }) + it('Should guard overload 3', () => { + // @ts-ignore + const N = Type.Union([]) // Never + const T = Type.Record(N, Type.String(), { extra: 1 }) + Assert.IsTrue(TypeGuard.IsRecord(T)) + Assert.IsTrue(TypeGuard.IsString(T.patternProperties[PatternNeverExact])) + Assert.IsEqual(T.extra, 1) + }) + it('Should guard overload 4', () => { + // @ts-ignore + const T = Type.Record(Type.BigInt(), Type.String(), { extra: 1 }) + Assert.IsTrue(TypeGuard.IsNever(T)) + }) + it('Should guard overload 5', () => { + const T = Type.Record(Type.Literal('A'), Type.String(), { extra: 1 }) + Assert.IsTrue(TypeGuard.IsObject(T)) + Assert.IsTrue(TypeGuard.IsString(T.properties.A)) + Assert.IsEqual(T.extra, 1) + }) + it('Should guard overload 6', () => { + const L = Type.TemplateLiteral([Type.Literal('hello'), Type.Union([Type.Literal('A'), Type.Literal('B')])]) + const T = Type.Record(L, Type.String(), { extra: 1 }) + Assert.IsTrue(TypeGuard.IsObject(T)) + Assert.IsTrue(TypeGuard.IsString(T.properties.helloA)) + Assert.IsTrue(TypeGuard.IsString(T.properties.helloB)) + Assert.IsEqual(T.extra, 1) + }) + it('Should guard overload 7', () => { + const T = Type.Record(Type.Number(), Type.String(), { extra: 1 }) + Assert.IsTrue(TypeGuard.IsRecord(T)) + Assert.IsTrue(TypeGuard.IsString(T.patternProperties[PatternNumberExact])) + Assert.IsEqual(T.extra, 1) + }) + it('Should guard overload 8', () => { + const T = Type.Record(Type.Integer(), Type.String(), { extra: 1 }) + Assert.IsTrue(TypeGuard.IsRecord(T)) + Assert.IsTrue(TypeGuard.IsString(T.patternProperties[PatternNumberExact])) + Assert.IsEqual(T.extra, 1) + }) + it('Should guard overload 9', () => { + const T = Type.Record(Type.String(), Type.String(), { extra: 1 }) + Assert.IsTrue(TypeGuard.IsRecord(T)) + Assert.IsTrue(TypeGuard.IsString(T.patternProperties[PatternStringExact])) + Assert.IsEqual(T.extra, 1) + }) + it('Should guard overload 10', () => { + const L = Type.TemplateLiteral([Type.String(), Type.Literal('_foo')]) + const T = Type.Record(L, Type.String(), { extra: 1 }) + Assert.IsTrue(TypeGuard.IsRecord(T)) + Assert.IsTrue(TypeGuard.IsString(T.patternProperties[`^${PatternString}_foo$`])) + Assert.IsEqual(T.extra, 1) + }) + it('Should guard overload 11', () => { + const L = Type.Union([Type.Literal('A'), Type.Union([Type.Literal('B'), Type.Literal('C')])]) + const T = Type.Record(L, Type.String()) + Assert.IsTrue(TypeGuard.IsObject(T)) + Assert.IsTrue(TypeGuard.IsString(T.properties.A)) + Assert.IsTrue(TypeGuard.IsString(T.properties.B)) + Assert.IsTrue(TypeGuard.IsString(T.properties.C)) + }) + it('Should guard overload 12', () => { + enum E { + A = 'X', + B = 'Y', + C = 'Z', + } + const T = Type.Enum(E) + const R = Type.Record(T, Type.Null()) + Assert.IsTrue(TypeGuard.IsObject(R)) + Assert.IsTrue(TypeGuard.IsNull(R.properties.X)) + Assert.IsTrue(TypeGuard.IsNull(R.properties.Y)) + Assert.IsTrue(TypeGuard.IsNull(R.properties.Z)) + }) + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/916 + // ---------------------------------------------------------------- + it('Should guard overload 13', () => { + // @ts-ignore + const T = Type.Record(Type.Never(), Type.String(), { extra: 1 }) + Assert.IsTrue(TypeGuard.IsRecord(T)) + Assert.IsTrue(TypeGuard.IsString(T.patternProperties[PatternNeverExact])) + Assert.IsEqual(T.extra, 1) + }) + it('Should guard overload 14', () => { + // @ts-ignore + const T = Type.Record(Type.Any(), Type.String(), { extra: 1 }) + Assert.IsTrue(TypeGuard.IsRecord(T)) + Assert.IsTrue(TypeGuard.IsString(T.patternProperties[PatternStringExact])) + Assert.IsEqual(T.extra, 1) + }) + // ------------------------------------------------------------- + // Variants + // ------------------------------------------------------------- + it('Should guard for TRecord', () => { + const R = TypeGuard.IsRecord(Type.Record(Type.String(), Type.Number())) + Assert.IsTrue(R) + }) + it('Should guard for TRecord with TObject value', () => { + const R = TypeGuard.IsRecord( + Type.Record( + Type.String(), + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + ), + ) + Assert.IsTrue(R) + }) + it('Should not guard for TRecord', () => { + const R = TypeGuard.IsRecord(null) + Assert.IsFalse(R) + }) + it('Should not guard for TRecord with invalid $id', () => { + // @ts-ignore + const R = TypeGuard.IsRecord(Type.Record(Type.String(), Type.Number(), { $id: 1 })) + Assert.IsFalse(R) + }) + it('Should not guard for TRecord with TObject value with invalid Property', () => { + const R = TypeGuard.IsRecord( + Type.Record( + Type.String(), + Type.Object({ + x: Type.Number(), + y: {} as any, + }), + ), + ) + Assert.IsFalse(R) + }) + it('Normalize: Should should normalize to TObject for single literal union value', () => { + const K = Type.Union([Type.Literal('ok')]) + const R = TypeGuard.IsObject(Type.Record(K, Type.Number())) + Assert.IsTrue(R) + }) + it('Normalize: Should should normalize to TObject for multi literal union value', () => { + const K = Type.Union([Type.Literal('A'), Type.Literal('B')]) + const R = TypeGuard.IsObject(Type.Record(K, Type.Number())) + Assert.IsTrue(R) + }) + // ------------------------------------------------------------------ + // Evaluated: Dollar Sign Escape + // https://github.com/sinclairzx81/typebox/issues/794 + // ------------------------------------------------------------------ + // prettier-ignore + { + const K = Type.TemplateLiteral('$prop${A|B|C}') // issue + const T = Type.Record(K, Type.String()) + Assert.IsTrue(TypeGuard.IsObject(T)) + Assert.IsTrue(TypeGuard.IsString(T.properties.$propA)) + Assert.IsTrue(TypeGuard.IsString(T.properties.$propB)) + Assert.IsTrue(TypeGuard.IsString(T.properties.$propC)) + Assert.IsEqual(T.required, ['$propA', '$propB', '$propC']) + } +}) diff --git a/test/runtime/type/guard/kind/recursive.ts b/test/runtime/type/guard/kind/recursive.ts new file mode 100644 index 000000000..81f8e41b9 --- /dev/null +++ b/test/runtime/type/guard/kind/recursive.ts @@ -0,0 +1,16 @@ +import { KindGuard, PatternNumberExact, PatternStringExact, PatternString, PatternNumber } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TRecursive', () => { + it('Should guard 1', () => { + const T = Type.Recursive((This) => Type.Object({ nodes: This })) + Assert.IsTrue(KindGuard.IsRecursive(T)) + Assert.IsTrue(KindGuard.IsObject(T)) + }) + it('Should guard 2', () => { + const T = Type.Recursive((This) => Type.Tuple([This])) + Assert.IsTrue(KindGuard.IsRecursive(T)) + Assert.IsTrue(KindGuard.IsTuple(T)) + }) +}) diff --git a/test/runtime/type/guard/kind/ref.ts b/test/runtime/type/guard/kind/ref.ts new file mode 100644 index 000000000..60d674634 --- /dev/null +++ b/test/runtime/type/guard/kind/ref.ts @@ -0,0 +1,36 @@ +import { KindGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TRef', () => { + // ---------------------------------------------------------------- + // Deprecated + // ---------------------------------------------------------------- + it('Should guard for Ref(Schema) 1', () => { + const T = Type.Number({ $id: 'T' }) + const R = Type.Ref(T) + Assert.IsTrue(KindGuard.IsRef(R)) + Assert.IsTrue(typeof R['$ref'] === 'string') + }) + it('Should guard for Ref(Schema) 2', () => { + const T = Type.Number() + Assert.Throws(() => Type.Ref(T)) + }) + it('Should guard for Ref(Schema) 3', () => { + // @ts-ignore + const T = Type.Number({ $id: null }) + Assert.Throws(() => Type.Ref(T)) + }) + // ---------------------------------------------------------------- + // Standard + // ---------------------------------------------------------------- + it('Should guard for TRef', () => { + const T = Type.Number({ $id: 'T' }) + const R = KindGuard.IsRef(Type.Ref(T)) + Assert.IsTrue(R) + }) + it('Should not guard for TRef', () => { + const R = KindGuard.IsRef(null) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/guard/kind/regexp.ts b/test/runtime/type/guard/kind/regexp.ts new file mode 100644 index 000000000..a4f38143a --- /dev/null +++ b/test/runtime/type/guard/kind/regexp.ts @@ -0,0 +1,18 @@ +import { KindGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TRegExp', () => { + it('Should guard for TRegExp 1', () => { + const T = Type.RegExp(/foo/, { $id: 'T' }) + Assert.IsTrue(KindGuard.IsSchema(T)) + }) + it('Should guard for TRegExp 1', () => { + const T = Type.RegExp(/foo/, { $id: 'T' }) + Assert.IsTrue(KindGuard.IsRegExp(T)) + }) + it('Should guard for TRegExp 2', () => { + const T = Type.RegExp('foo', { $id: 'T' }) + Assert.IsTrue(KindGuard.IsRegExp(T)) + }) +}) diff --git a/test/runtime/type/guard/kind/required.ts b/test/runtime/type/guard/kind/required.ts new file mode 100644 index 000000000..0bf29e49e --- /dev/null +++ b/test/runtime/type/guard/kind/required.ts @@ -0,0 +1,96 @@ +import { KindGuard, TypeRegistry, Type, Kind, TransformKind } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TRequired', () => { + it('Should produce a valid TSchema', () => { + const T = Type.Required(Type.Object({ x: Type.Number() })) + Assert.IsTrue(KindGuard.IsSchema(T)) + }) + it('Should support TUnsafe required properties with no Kind', () => { + const T = Type.Required(Type.Object({ x: Type.Optional(Type.Unsafe({ x: 1 })) })) + Assert.IsEqual(T.required, ['x']) + }) + it('Should support TUnsafe required properties with unknown Kind', () => { + const T = Type.Required(Type.Object({ x: Type.Optional(Type.Unsafe({ [Kind]: 'UnknownRequiredType', x: 1 })) })) + Assert.IsEqual(T.required, ['x']) + }) + it('Should support TUnsafe required properties with known Kind', () => { + TypeRegistry.Set('KnownRequiredType', () => true) + const T = Type.Required(Type.Object({ x: Type.Optional(Type.Unsafe({ [Kind]: 'KnownRequiredType', x: 1 })) })) + Assert.IsEqual(T.required, ['x']) + }) + it('Should support applying required to intersect', () => { + const A = Type.Object({ x: Type.Optional(Type.Number()) }) + const B = Type.Object({ y: Type.Optional(Type.Number()) }) + const I = Type.Intersect([A, B]) + const T = Type.Required(I) + Assert.IsEqual(T.allOf.length, 2) + Assert.IsEqual(T.allOf[0].required, ['x']) + Assert.IsEqual(T.allOf[1].required, ['y']) + }) + it('Should support applying required to union', () => { + const A = Type.Object({ x: Type.Optional(Type.Number()) }) + const B = Type.Object({ y: Type.Optional(Type.Number()) }) + const I = Type.Union([A, B]) + const T = Type.Required(I) + Assert.IsEqual(T.anyOf.length, 2) + Assert.IsEqual(T.anyOf[0].required, ['x']) + Assert.IsEqual(T.anyOf[1].required, ['y']) + }) + // ---------------------------------------------------------------- + // Discard + // ---------------------------------------------------------------- + it('Should override $id', () => { + const A = Type.Object({ x: Type.Number() }, { $id: 'A' }) + const T = Type.Required(A, { $id: 'T' }) + Assert.IsEqual(T.$id!, 'T') + }) + it('Should discard $id', () => { + const A = Type.Object({ x: Type.Number() }, { $id: 'A' }) + const T = Type.Required(A) + Assert.IsFalse('$id' in T) + }) + it('Should discard transform', () => { + const T = Type.Object({ + x: Type.Number(), + y: Type.String(), + }) + const S = Type.Transform(T) + .Decode((value) => value) + .Encode((value) => value) + const R = Type.Required(S) + Assert.IsFalse(TransformKind in R) + }) + // ------------------------------------------------------------------ + // Intrinsic Passthough + // https://github.com/sinclairzx81/typebox/issues/1169 + // ------------------------------------------------------------------ + it('Should pass through on intrinsic types on union 1', () => { + const T = Type.Required( + Type.Union([ + Type.Number(), + Type.Object({ + x: Type.Optional(Type.Number()), + }), + ]), + ) + Assert.IsTrue(KindGuard.IsUnion(T)) + Assert.IsTrue(KindGuard.IsNumber(T.anyOf[0])) + Assert.IsTrue(KindGuard.IsObject(T.anyOf[1])) + Assert.IsFalse(KindGuard.IsOptional(T.anyOf[1].properties.x)) + }) + it('Should pass through on intrinsic types on union 2', () => { + const T = Type.Required( + Type.Union([ + Type.Literal(1), + Type.Object({ + x: Type.Optional(Type.Number()), + }), + ]), + ) + Assert.IsTrue(KindGuard.IsUnion(T)) + Assert.IsTrue(KindGuard.IsLiteral(T.anyOf[0])) + Assert.IsTrue(KindGuard.IsObject(T.anyOf[1])) + Assert.IsFalse(KindGuard.IsOptional(T.anyOf[1].properties.x)) + }) +}) diff --git a/test/runtime/type/guard/kind/rest.ts b/test/runtime/type/guard/kind/rest.ts new file mode 100644 index 000000000..bb63a4702 --- /dev/null +++ b/test/runtime/type/guard/kind/rest.ts @@ -0,0 +1,59 @@ +import { Type, KindGuard } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TRest', () => { + it('Should guard 1', () => { + // union never + const A = Type.String() + const B = Type.Union(Type.Rest(A)) + Assert.IsTrue(KindGuard.IsNever(B)) + }) + it('Should guard 2', () => { + // intersect never + const A = Type.String() + const B = Type.Intersect(Type.Rest(A)) + Assert.IsTrue(KindGuard.IsNever(B)) + }) + it('Should guard 3', () => { + // tuple + const A = Type.Tuple([Type.Number(), Type.String()]) + const B = Type.Union(Type.Rest(A)) + Assert.IsTrue(KindGuard.IsUnion(B)) + Assert.IsEqual(B.anyOf.length, 2) + Assert.IsTrue(KindGuard.IsNumber(B.anyOf[0])) + Assert.IsTrue(KindGuard.IsString(B.anyOf[1])) + }) + it('Should guard 4', () => { + // tuple spread + const A = Type.Tuple([Type.Literal(1), Type.Literal(2)]) + const B = Type.Tuple([Type.Literal(3), Type.Literal(4)]) + const C = Type.Tuple([...Type.Rest(A), ...Type.Rest(B)]) + Assert.IsTrue(KindGuard.IsTuple(C)) + Assert.IsEqual(C.items!.length, 4) + Assert.IsEqual(C.items![0].const, 1) + Assert.IsEqual(C.items![1].const, 2) + Assert.IsEqual(C.items![2].const, 3) + Assert.IsEqual(C.items![3].const, 4) + }) + it('Should guard 5', () => { + // union to intersect + const A = Type.Object({ x: Type.Number() }) + const B = Type.Object({ y: Type.String() }) + const C = Type.Union([A, B]) + const D = Type.Intersect(Type.Rest(C)) + Assert.IsTrue(KindGuard.IsIntersect(D)) + Assert.IsEqual(D.allOf.length, 2) + Assert.IsTrue(KindGuard.IsObject(D.allOf[0])) + Assert.IsTrue(KindGuard.IsObject(D.allOf[1])) + }) + it('Should guard 6', () => { + // intersect to composite + const A = Type.Object({ x: Type.Number() }) + const B = Type.Object({ y: Type.String() }) + const C = Type.Intersect([A, B]) + const D = Type.Composite(Type.Rest(C)) + Assert.IsTrue(KindGuard.IsObject(D)) + Assert.IsTrue(KindGuard.IsNumber(D.properties.x)) + Assert.IsTrue(KindGuard.IsString(D.properties.y)) + }) +}) diff --git a/test/runtime/type/guard/kind/string.ts b/test/runtime/type/guard/kind/string.ts new file mode 100644 index 000000000..ca9b5917a --- /dev/null +++ b/test/runtime/type/guard/kind/string.ts @@ -0,0 +1,14 @@ +import { KindGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TString', () => { + it('Should guard for TString', () => { + const R = KindGuard.IsString(Type.String()) + Assert.IsTrue(R) + }) + it('Should not guard for TString', () => { + const R = KindGuard.IsString(null) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/guard/kind/symbol.ts b/test/runtime/type/guard/kind/symbol.ts new file mode 100644 index 000000000..f58924b07 --- /dev/null +++ b/test/runtime/type/guard/kind/symbol.ts @@ -0,0 +1,14 @@ +import { KindGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TSymbol', () => { + it('Should guard for TSymbol', () => { + const R = KindGuard.IsSymbol(Type.Symbol()) + Assert.IsTrue(R) + }) + it('Should not guard for TSymbol', () => { + const R = KindGuard.IsSymbol(null) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/guard/kind/template-literal.ts b/test/runtime/type/guard/kind/template-literal.ts new file mode 100644 index 000000000..9c01c310d --- /dev/null +++ b/test/runtime/type/guard/kind/template-literal.ts @@ -0,0 +1,35 @@ +import { KindGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TTemplateLiteral', () => { + it('Should guard for empty TemplateLiteral', () => { + const R = KindGuard.IsTemplateLiteral(Type.TemplateLiteral([])) + Assert.IsTrue(R) + }) + it('Should guard for TSchema', () => { + const R = KindGuard.IsSchema(Type.TemplateLiteral([])) + Assert.IsTrue(R) + }) + it('Should guard for TemplateLiteral (TTemplateLiteral)', () => { + const T = Type.TemplateLiteral([Type.Literal('hello')]) + const R = KindGuard.IsTemplateLiteral(Type.TemplateLiteral([T, Type.Literal('world')])) + Assert.IsTrue(R) + }) + it('Should guard for TemplateLiteral (TLiteral)', () => { + const R = KindGuard.IsTemplateLiteral(Type.TemplateLiteral([Type.Literal('hello')])) + Assert.IsTrue(R) + }) + it('Should guard for TemplateLiteral (TString)', () => { + const R = KindGuard.IsTemplateLiteral(Type.TemplateLiteral([Type.String()])) + Assert.IsTrue(R) + }) + it('Should guard for TemplateLiteral (TNumber)', () => { + const R = KindGuard.IsTemplateLiteral(Type.TemplateLiteral([Type.Number()])) + Assert.IsTrue(R) + }) + it('Should guard for TemplateLiteral (TBoolean)', () => { + const R = KindGuard.IsTemplateLiteral(Type.TemplateLiteral([Type.Boolean()])) + Assert.IsTrue(R) + }) +}) diff --git a/test/runtime/type/guard/kind/this.ts b/test/runtime/type/guard/kind/this.ts new file mode 100644 index 000000000..ee1cd4dcb --- /dev/null +++ b/test/runtime/type/guard/kind/this.ts @@ -0,0 +1,13 @@ +import { KindGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TThis', () => { + it('Should guard for TThis', () => { + Type.Recursive((This) => { + const R = KindGuard.IsThis(This) + Assert.IsTrue(R) + return Type.Object({ nodes: Type.Array(This) }) + }) + }) +}) diff --git a/test/runtime/type/guard/kind/tuple.ts b/test/runtime/type/guard/kind/tuple.ts new file mode 100644 index 000000000..289ba57ae --- /dev/null +++ b/test/runtime/type/guard/kind/tuple.ts @@ -0,0 +1,14 @@ +import { KindGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TTuple', () => { + it('Should guard for TTuple', () => { + const R = KindGuard.IsTuple(Type.Tuple([Type.Number(), Type.Number()])) + Assert.IsTrue(R) + }) + it('Should not guard for TTuple', () => { + const R = KindGuard.IsTuple(null) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/guard/kind/uint8array.ts b/test/runtime/type/guard/kind/uint8array.ts new file mode 100644 index 000000000..f8af30701 --- /dev/null +++ b/test/runtime/type/guard/kind/uint8array.ts @@ -0,0 +1,14 @@ +import { KindGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TUint8Array', () => { + it('Should guard for TUint8Array', () => { + const R = KindGuard.IsUint8Array(Type.Uint8Array()) + Assert.IsTrue(R) + }) + it('Should not guard for TUint8Array', () => { + const R = KindGuard.IsUint8Array(null) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/guard/kind/uncapitalize.ts b/test/runtime/type/guard/kind/uncapitalize.ts new file mode 100644 index 000000000..65c75b2c4 --- /dev/null +++ b/test/runtime/type/guard/kind/uncapitalize.ts @@ -0,0 +1,33 @@ +import { KindGuard, Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/Uncapitalize', () => { + it('Should guard for Uncapitalize 1', () => { + const T = Type.Uncapitalize(Type.Literal('HELLO'), { $id: 'hello', foo: 1 }) + Assert.IsTrue(KindGuard.IsLiteral(T)) + Assert.IsEqual(T.const, 'hELLO') + Assert.IsEqual(T.$id, 'hello') + Assert.IsEqual(T.foo, 1) + }) + it('Should guard for Uncapitalize 2', () => { + const T = Type.Uncapitalize(Type.Literal('HELLO')) + Assert.IsTrue(KindGuard.IsLiteral(T)) + Assert.IsEqual(T.const, 'hELLO') + }) + it('Should guard for Uncapitalize 3', () => { + const T = Type.Uncapitalize(Type.Union([Type.Literal('HELLO'), Type.Literal('WORLD')])) + Assert.IsTrue(KindGuard.IsUnion(T)) + Assert.IsEqual(T.anyOf[0].const, 'hELLO') + Assert.IsEqual(T.anyOf[1].const, 'wORLD') + }) + it('Should guard for Uncapitalize 4', () => { + const T = Type.Uncapitalize(Type.TemplateLiteral('HELLO${0|1}')) + Assert.IsTrue(KindGuard.IsTemplateLiteral(T)) + Assert.IsEqual(T.pattern, '^(hELLO0|hELLO1)$') + }) + it('Should guard for Uncapitalize 5', () => { + const T = Type.Uncapitalize(Type.TemplateLiteral([Type.Literal('HELLO'), Type.Union([Type.Literal(0), Type.Literal(1)])])) + Assert.IsTrue(KindGuard.IsTemplateLiteral(T)) + Assert.IsEqual(T.pattern, '^(hELLO0|hELLO1)$') + }) +}) diff --git a/test/runtime/type/guard/kind/undefined.ts b/test/runtime/type/guard/kind/undefined.ts new file mode 100644 index 000000000..1c7aebbcf --- /dev/null +++ b/test/runtime/type/guard/kind/undefined.ts @@ -0,0 +1,14 @@ +import { KindGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TUndefined', () => { + it('Should guard for TUndefined', () => { + const R = KindGuard.IsUndefined(Type.Undefined()) + Assert.IsTrue(R) + }) + it('Should not guard for TUndefined', () => { + const R = KindGuard.IsUndefined(null) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/guard/kind/union.ts b/test/runtime/type/guard/kind/union.ts new file mode 100644 index 000000000..03e934fd5 --- /dev/null +++ b/test/runtime/type/guard/kind/union.ts @@ -0,0 +1,42 @@ +import { TSchema, KindGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TUnion', () => { + it('Should guard for TUnion', () => { + const R = KindGuard.IsUnion( + Type.Union([ + Type.Object({ + x: Type.Number(), + }), + Type.Object({ + y: Type.Number(), + }), + ]), + ) + Assert.IsTrue(R) + }) + it('Should not guard for TUnion', () => { + const R = KindGuard.IsUnion(null) + Assert.IsFalse(R) + }) + it('Transform: Should transform to never for zero length union', () => { + const T = Type.Union([]) + const R = KindGuard.IsNever(T) + Assert.IsTrue(R) + }) + it('Transform: Should unwrap union type for array of length === 1', () => { + const T = Type.Union([Type.String()]) + const R = KindGuard.IsString(T) + Assert.IsTrue(R) + }) + it('Transform: Should retain union if array length > 1', () => { + const T = Type.Union([Type.String(), Type.Number()]) + const R1 = KindGuard.IsUnion(T) + const R2 = KindGuard.IsString(T.anyOf[0]) + const R3 = KindGuard.IsNumber(T.anyOf[1]) + Assert.IsTrue(R1) + Assert.IsTrue(R2) + Assert.IsTrue(R3) + }) +}) diff --git a/test/runtime/type/guard/kind/unknown.ts b/test/runtime/type/guard/kind/unknown.ts new file mode 100644 index 000000000..066414e69 --- /dev/null +++ b/test/runtime/type/guard/kind/unknown.ts @@ -0,0 +1,14 @@ +import { KindGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TUnknown', () => { + it('Should guard for TUnknown', () => { + const R = KindGuard.IsUnknown(Type.Unknown()) + Assert.IsTrue(R) + }) + it('Should not guard for TUnknown', () => { + const R = KindGuard.IsUnknown(null) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/guard/kind/unsafe.ts b/test/runtime/type/guard/kind/unsafe.ts new file mode 100644 index 000000000..c0f206d7e --- /dev/null +++ b/test/runtime/type/guard/kind/unsafe.ts @@ -0,0 +1,33 @@ +import { Kind, KindGuard, TypeRegistry } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TUnsafe', () => { + it('Should guard raw TUnsafe', () => { + const T = Type.Unsafe({ x: 1 }) + const R = KindGuard.IsUnsafe(T) + Assert.IsTrue(R) + }) + it('Should guard raw TUnsafe as TSchema', () => { + const T = Type.Unsafe({ x: 1 }) + const R = KindGuard.IsSchema(T) + Assert.IsTrue(R) + }) + it('Should guard override TUnsafe as TSchema when registered', () => { + TypeRegistry.Set('UnsafeType', () => true) + const T = Type.Unsafe({ [Kind]: 'UnsafeType' }) + const R = KindGuard.IsSchema(T) + Assert.IsTrue(R) + TypeRegistry.Delete('UnsafeType') + }) + it('Should not guard TUnsafe with unregistered kind', () => { + const T = Type.Unsafe({ [Kind]: 'UnsafeType' }) + const R = KindGuard.IsUnsafe(T) + Assert.IsFalse(R) + }) + it('Should not guard for TString', () => { + const T = Type.String() + const R = KindGuard.IsUnsafe(T) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/guard/kind/uppercase.ts b/test/runtime/type/guard/kind/uppercase.ts new file mode 100644 index 000000000..fc8a79603 --- /dev/null +++ b/test/runtime/type/guard/kind/uppercase.ts @@ -0,0 +1,33 @@ +import { KindGuard, Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/Uppercase', () => { + it('Should guard for Uppercase 1', () => { + const T = Type.Uppercase(Type.Literal('hello'), { $id: 'hello', foo: 1 }) + Assert.IsTrue(KindGuard.IsLiteral(T)) + Assert.IsEqual(T.const, 'HELLO') + Assert.IsEqual(T.$id, 'hello') + Assert.IsEqual(T.foo, 1) + }) + it('Should guard for Uppercase 2', () => { + const T = Type.Uppercase(Type.Literal('hello')) + Assert.IsTrue(KindGuard.IsLiteral(T)) + Assert.IsEqual(T.const, 'HELLO') + }) + it('Should guard for Uppercase 3', () => { + const T = Type.Uppercase(Type.Union([Type.Literal('hello'), Type.Literal('world')])) + Assert.IsTrue(KindGuard.IsUnion(T)) + Assert.IsEqual(T.anyOf[0].const, 'HELLO') + Assert.IsEqual(T.anyOf[1].const, 'WORLD') + }) + it('Should guard for Uppercase 4', () => { + const T = Type.Uppercase(Type.TemplateLiteral('hello${0|1}')) + Assert.IsTrue(KindGuard.IsTemplateLiteral(T)) + Assert.IsEqual(T.pattern, '^(HELLO0|HELLO1)$') + }) + it('Should guard for Uppercase 5', () => { + const T = Type.Uppercase(Type.TemplateLiteral([Type.Literal('hello'), Type.Union([Type.Literal(0), Type.Literal(1)])])) + Assert.IsTrue(KindGuard.IsTemplateLiteral(T)) + Assert.IsEqual(T.pattern, '^(HELLO0|HELLO1)$') + }) +}) diff --git a/test/runtime/type/guard/kind/void.ts b/test/runtime/type/guard/kind/void.ts new file mode 100644 index 000000000..40370e079 --- /dev/null +++ b/test/runtime/type/guard/kind/void.ts @@ -0,0 +1,14 @@ +import { KindGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/kind/TVoid', () => { + it('Should guard for TVoid', () => { + const R = KindGuard.IsVoid(Type.Void()) + Assert.IsTrue(R) + }) + it('Should not guard for TVoid', () => { + const R = KindGuard.IsVoid(null) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/guard/type/any.ts b/test/runtime/type/guard/type/any.ts new file mode 100644 index 000000000..6eebd946d --- /dev/null +++ b/test/runtime/type/guard/type/any.ts @@ -0,0 +1,19 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TAny', () => { + it('Should guard for TAny', () => { + const R = TypeGuard.IsAny(Type.Any()) + Assert.IsTrue(R) + }) + it('Should not guard for TAny', () => { + const R = TypeGuard.IsAny(null) + Assert.IsFalse(R) + }) + it('Should not guard for TAny with invalid $id', () => { + // @ts-ignore + const R = TypeGuard.IsAny(Type.Any({ $id: 1 })) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/guard/type/argument.ts b/test/runtime/type/guard/type/argument.ts new file mode 100644 index 000000000..904cf8068 --- /dev/null +++ b/test/runtime/type/guard/type/argument.ts @@ -0,0 +1,14 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TArgument', () => { + it('Should guard for TArgument', () => { + const R = TypeGuard.IsArgument(Type.Argument(0)) + Assert.IsTrue(R) + }) + it('Should not guard for TArgument', () => { + const R = TypeGuard.IsArgument(null) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/guard/type/array.ts b/test/runtime/type/guard/type/array.ts new file mode 100644 index 000000000..fe1033754 --- /dev/null +++ b/test/runtime/type/guard/type/array.ts @@ -0,0 +1,56 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TArray', () => { + it('Should guard for TArray', () => { + const R = TypeGuard.IsArray(Type.Array(Type.Number())) + Assert.IsTrue(R) + }) + it('Should not guard for TArray', () => { + const R = TypeGuard.IsArray(null) + Assert.IsFalse(R) + }) + it('Should guard for nested object TArray', () => { + const R = TypeGuard.IsArray( + Type.Array( + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + ), + ) + Assert.IsTrue(R) + }) + it('Should not guard for nested object TArray', () => { + const R = TypeGuard.IsArray( + Type.Array( + Type.Object({ + x: Type.Number(), + y: {} as any, + }), + ), + ) + Assert.IsFalse(R) + }) + it('Should not guard for TArray with invalid $id', () => { + // @ts-ignore + const R = TypeGuard.IsArray(Type.Array(Type.Number(), { $id: 1 })) + Assert.IsFalse(R) + }) + it('Should not guard for TArray with invalid minItems', () => { + // @ts-ignore + const R = TypeGuard.IsArray(Type.Array(Type.String(), { minItems: '1' })) + Assert.IsFalse(R) + }) + it('Should not guard for TArray with invalid maxItems', () => { + // @ts-ignore + const R = TypeGuard.IsArray(Type.Array(Type.String(), { maxItems: '1' })) + Assert.IsFalse(R) + }) + it('Should not guard for TArray with invalid uniqueItems', () => { + // @ts-ignore + const R = TypeGuard.IsArray(Type.Array(Type.String(), { uniqueItems: '1' })) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/guard/type/async-iterator.ts b/test/runtime/type/guard/type/async-iterator.ts new file mode 100644 index 000000000..cde73726f --- /dev/null +++ b/test/runtime/type/guard/type/async-iterator.ts @@ -0,0 +1,22 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TAsyncIterator', () => { + it('Should guard for TAsyncIterator', () => { + const T = Type.AsyncIterator(Type.Any()) + const R = TypeGuard.IsAsyncIterator(T) + Assert.IsTrue(R) + }) + it('Should not guard for TAsyncIterator', () => { + const T = null + const R = TypeGuard.IsAsyncIterator(T) + Assert.IsFalse(R) + }) + it('Should not guard for TAsyncIterator with invalid $id', () => { + //@ts-ignore + const T = Type.AsyncIterator(Type.Any(), { $id: 1 }) + const R = TypeGuard.IsAsyncIterator(T) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/guard/type/awaited.ts b/test/runtime/type/guard/type/awaited.ts new file mode 100644 index 000000000..fbfe4dc09 --- /dev/null +++ b/test/runtime/type/guard/type/awaited.ts @@ -0,0 +1,41 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/Awaited', () => { + it('Should guard for Awaited 1', () => { + const T = Type.Awaited(Type.String()) + const R = TypeGuard.IsString(T) + Assert.IsTrue(R) + }) + it('Should guard for Awaited 2', () => { + const T = Type.Awaited(Type.Promise(Type.String())) + const R = TypeGuard.IsString(T) + Assert.IsTrue(R) + }) + it('Should guard for Awaited 3', () => { + const T = Type.Awaited(Type.Awaited(Type.Promise(Type.String()))) + const R = TypeGuard.IsString(T) + Assert.IsTrue(R) + }) + it('Should guard for Awaited 4', () => { + const T = Type.Awaited(Type.Union([Type.Promise(Type.Promise(Type.String()))])) + Assert.IsTrue(TypeGuard.IsString(T)) + }) + it('Should guard for Awaited 5', () => { + const T = Type.Awaited(Type.Union([Type.Promise(Type.Promise(Type.String())), Type.Number()])) + Assert.IsTrue(TypeGuard.IsUnion(T)) + Assert.IsTrue(TypeGuard.IsString(T.anyOf[0])) + Assert.IsTrue(TypeGuard.IsNumber(T.anyOf[1])) + }) + it('Should guard for Awaited 6', () => { + const T = Type.Awaited(Type.Intersect([Type.Promise(Type.Promise(Type.String()))])) + Assert.IsTrue(TypeGuard.IsString(T)) + }) + it('Should guard for Awaited 7', () => { + const T = Type.Awaited(Type.Intersect([Type.Promise(Type.Promise(Type.String())), Type.Number()])) + Assert.IsTrue(TypeGuard.IsIntersect(T)) + Assert.IsTrue(TypeGuard.IsString(T.allOf[0])) + Assert.IsTrue(TypeGuard.IsNumber(T.allOf[1])) + }) +}) diff --git a/test/runtime/type/guard/type/bigint.ts b/test/runtime/type/guard/type/bigint.ts new file mode 100644 index 000000000..fa9eb3b7c --- /dev/null +++ b/test/runtime/type/guard/type/bigint.ts @@ -0,0 +1,19 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TBigInt', () => { + it('Should guard for TBigInt', () => { + const R = TypeGuard.IsBigInt(Type.BigInt()) + Assert.IsTrue(R) + }) + it('Should not guard for TBigInt', () => { + const R = TypeGuard.IsBigInt(null) + Assert.IsFalse(R) + }) + it('Should not guard for BigInt with invalid $id', () => { + // @ts-ignore + const R = TypeGuard.IsBigInt(Type.BigInt({ $id: 1 })) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/guard/type/boolean.ts b/test/runtime/type/guard/type/boolean.ts new file mode 100644 index 000000000..27c9df1aa --- /dev/null +++ b/test/runtime/type/guard/type/boolean.ts @@ -0,0 +1,19 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TBoolean', () => { + it('Should guard for TBoolean', () => { + const R = TypeGuard.IsBoolean(Type.Boolean()) + Assert.IsTrue(R) + }) + it('Should not guard for TBoolean', () => { + const R = TypeGuard.IsBoolean(null) + Assert.IsFalse(R) + }) + it('Should not guard for TBoolean with invalid $id', () => { + // @ts-ignore + const R = TypeGuard.IsBoolean(Type.Boolean({ $id: 1 })) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/guard/type/capitalize.ts b/test/runtime/type/guard/type/capitalize.ts new file mode 100644 index 000000000..c34b0849d --- /dev/null +++ b/test/runtime/type/guard/type/capitalize.ts @@ -0,0 +1,33 @@ +import { TypeGuard, Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/Capitalize', () => { + it('Should guard for Capitalize 1', () => { + const T = Type.Capitalize(Type.Literal('hello'), { $id: 'hello', foo: 1 }) + Assert.IsTrue(TypeGuard.IsLiteral(T)) + Assert.IsEqual(T.const, 'Hello') + Assert.IsEqual(T.$id, 'hello') + Assert.IsEqual(T.foo, 1) + }) + it('Should guard for Capitalize 2', () => { + const T = Type.Capitalize(Type.Literal('hello')) + Assert.IsTrue(TypeGuard.IsLiteral(T)) + Assert.IsEqual(T.const, 'Hello') + }) + it('Should guard for Capitalize 3', () => { + const T = Type.Capitalize(Type.Union([Type.Literal('hello'), Type.Literal('world')])) + Assert.IsTrue(TypeGuard.IsUnion(T)) + Assert.IsEqual(T.anyOf[0].const, 'Hello') + Assert.IsEqual(T.anyOf[1].const, 'World') + }) + it('Should guard for Capitalize 4', () => { + const T = Type.Capitalize(Type.TemplateLiteral('hello${0|1}')) + Assert.IsTrue(TypeGuard.IsTemplateLiteral(T)) + Assert.IsEqual(T.pattern, '^(Hello0|Hello1)$') + }) + it('Should guard for Capitalize 5', () => { + const T = Type.Capitalize(Type.TemplateLiteral([Type.Literal('hello'), Type.Union([Type.Literal(0), Type.Literal(1)])])) + Assert.IsTrue(TypeGuard.IsTemplateLiteral(T)) + Assert.IsEqual(T.pattern, '^(Hello0|Hello1)$') + }) +}) diff --git a/test/runtime/type/guard/type/composite.ts b/test/runtime/type/guard/type/composite.ts new file mode 100644 index 000000000..1cada8065 --- /dev/null +++ b/test/runtime/type/guard/type/composite.ts @@ -0,0 +1,165 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TComposite', () => { + it('Should guard for distinct properties', () => { + const T = Type.Composite([Type.Object({ x: Type.Number() }), Type.Object({ y: Type.Number() })]) + Assert.IsTrue(TypeGuard.IsNumber(T.properties.x)) + Assert.IsTrue(TypeGuard.IsNumber(T.properties.y)) + }) + it('Should guard for overlapping properties', () => { + const T = Type.Composite([Type.Object({ x: Type.Number() }), Type.Object({ x: Type.Number() })]) + Assert.IsTrue(TypeGuard.IsIntersect(T.properties.x)) + // @ts-ignore + Assert.IsTrue(TypeGuard.IsNumber(T.properties.x.allOf[0])) + // @ts-ignore + Assert.IsTrue(TypeGuard.IsNumber(T.properties.x.allOf[1])) + }) + it('Should not produce optional property if all properties are not optional', () => { + const T = Type.Composite([Type.Object({ x: Type.Optional(Type.Number()) }), Type.Object({ x: Type.Number() })]) + Assert.IsFalse(TypeGuard.IsOptional(T.properties.x)) + }) + // Note for: https://github.com/sinclairzx81/typebox/issues/419 + // Determining if a composite property is optional requires a deep check for all properties gathered during a indexed access + // call. Currently, there isn't a trivial way to perform this check without running into possibly infinite instantiation issues. + // The optional check is only specific to overlapping properties. Singular properties will continue to work as expected. The + // rule is "if all composite properties for a key are optional, then the composite property is optional". Defer this test and + // document as minor breaking change. + // + it('Should produce optional property if all composited properties are optional', () => { + // prettier-ignore + const T = Type.Composite([ + Type.Object({ x: Type.Optional(Type.Number()) }), + Type.Object({ x: Type.Optional(Type.Number()) }) + ]) + Assert.IsTrue(TypeGuard.IsOptional(T.properties.x)) + Assert.IsEqual(T.required, undefined) + }) + // prettier-ignore + it('Should produce required property if some composited properties are not optional', () => { + const T = Type.Composite([ + Type.Object({ x: Type.Optional(Type.Number()) }), + Type.Object({ x: Type.Number() }) + ]) + Assert.IsFalse(TypeGuard.IsOptional(T.properties.x)) + Assert.IsTrue(T.required!.includes('x')) + }) + // prettier-ignore + it('Should preserve single optional property', () => { + const T = Type.Composite([ + Type.Object({ x: Type.Optional(Type.Number()) }), + ]) + Assert.IsTrue(TypeGuard.IsOptional(T.properties.x)) + Assert.IsEqual(T.required, undefined) + }) + // ---------------------------------------------------------------- + // Intersect + // ---------------------------------------------------------------- + // prettier-ignore + it('Should composite Intersect 1', () => { + const T = Type.Composite([ + Type.Intersect([ + Type.Object({ x: Type.Number() }), + Type.Object({ y: Type.Number() }), + ]), + Type.Intersect([ + Type.Object({ z: Type.Number() }), + ]) + ]) + Assert.IsEqual(T, Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number() + })) + }) + // prettier-ignore + it('Should composite Intersect 2', () => { + const T = Type.Composite([ + Type.Intersect([ + Type.Object({ x: Type.Number() }), + Type.Object({ x: Type.Number() }), + ]), + Type.Intersect([ + Type.Object({ x: Type.Number() }), + ]) + ]) + Assert.IsEqual(T, Type.Object({ + x: Type.Intersect([Type.Intersect([Type.Number(), Type.Number()]), Type.Number()]) + })) + }) + // prettier-ignore + it('Should composite Intersect 3', () => { + const T = Type.Composite([ + Type.Number(), + Type.Boolean() + ]) + Assert.IsEqual(T, Type.Object({})) + }) + // prettier-ignore + it('Should composite Intersect 4', () => { + const T = Type.Composite([ + Type.Number(), + Type.Boolean(), + Type.Object({ x: Type.String() }) + ]) + Assert.IsEqual(T, Type.Object({ + x: Type.String() + })) + }) + // prettier-ignore + it('Should composite Intersect 5', () => { + const T = Type.Composite([ + Type.Object({ x: Type.Optional(Type.String()) }), + Type.Object({ x: Type.String() }) + ]) + Assert.IsEqual(T, Type.Object({ + x: Type.Intersect([Type.String(), Type.String()]) + })) + }) + // prettier-ignore + it('Should composite Intersect 6', () => { + const T = Type.Composite([ + Type.Object({ x: Type.Optional(Type.String()) }), + Type.Object({ x: Type.Optional(Type.String()) }) + ]) + Assert.IsEqual(T, Type.Object({ + x: Type.Optional(Type.Intersect([Type.String(), Type.String()])) + })) + }) + // ---------------------------------------------------------------- + // Union + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/789 + // prettier-ignore + it('Should composite Union 1 (non-overlapping)', () => { + const T = Type.Composite([ + Type.Union([ + Type.Object({ x: Type.Number() }), + Type.Object({ y: Type.Number() }), + ]), + Type.Union([ + Type.Object({ z: Type.Number() }), + ]) + ]) + Assert.IsEqual(T, Type.Object({ + z: Type.Number() + })) + }) + // https://github.com/sinclairzx81/typebox/issues/789 + // prettier-ignore + it('Should composite Union 2 (overlapping)', () => { + const T = Type.Composite([ + Type.Union([ + Type.Object({ x: Type.Number() }), + Type.Object({ x: Type.Number() }), + ]), + Type.Union([ + Type.Object({ x: Type.Number() }), + ]) + ]) + Assert.IsEqual(T, Type.Object({ + x: Type.Intersect([Type.Union([Type.Number(), Type.Number()]), Type.Number()]) + })) + }) +}) diff --git a/test/runtime/type/guard/type/computed.ts b/test/runtime/type/guard/type/computed.ts new file mode 100644 index 000000000..d4dd6df24 --- /dev/null +++ b/test/runtime/type/guard/type/computed.ts @@ -0,0 +1,33 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TComputed', () => { + // ---------------------------------------------------------------- + // Schema + // ---------------------------------------------------------------- + it('Should guard for Schema', () => { + const T = Type.Partial(Type.Ref('A')) + Assert.IsTrue(TypeGuard.IsComputed(T)) + Assert.IsTrue(TypeGuard.IsSchema(T)) + }) + // ---------------------------------------------------------------- + // Record + // ---------------------------------------------------------------- + it('Should guard for Record 1', () => { + const T = Type.Record(Type.String(), Type.String()) + Assert.IsTrue(TypeGuard.IsRecord(T)) + }) + it('Should guard for Record 3', () => { + const T = Type.Record(Type.String(), Type.Ref('A')) + Assert.IsTrue(TypeGuard.IsRecord(T)) + }) + it('Should guard for Record 3', () => { + const T = Type.Record(Type.String(), Type.Partial(Type.Ref('A'))) + Assert.IsTrue(TypeGuard.IsRecord(T)) + }) + it('Should guard for Record 4', () => { + const T = Type.Record(Type.Ref('A'), Type.String()) + Assert.IsTrue(TypeGuard.IsNever(T)) + }) +}) diff --git a/test/runtime/type/guard/type/const.ts b/test/runtime/type/guard/type/const.ts new file mode 100644 index 000000000..408075c37 --- /dev/null +++ b/test/runtime/type/guard/type/const.ts @@ -0,0 +1,124 @@ +import { TypeGuard, ValueGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TConstT', () => { + // ---------------------------------------------------------------- + // Identity Types + // ---------------------------------------------------------------- + it('Should guard for TConst 1', () => { + const T = Type.Const(undefined) + Assert.IsFalse(TypeGuard.IsReadonly(T)) + Assert.IsTrue(TypeGuard.IsUndefined(T)) + }) + it('Should guard for TConst 2', () => { + const T = Type.Const(null) + Assert.IsFalse(TypeGuard.IsReadonly(T)) + Assert.IsTrue(TypeGuard.IsNull(T)) + }) + it('Should guard for TConst 3', () => { + const T = Type.Const(Symbol()) + Assert.IsFalse(TypeGuard.IsReadonly(T)) + Assert.IsTrue(TypeGuard.IsSymbol(T)) + }) + it('Should guard for TConst 4', () => { + const T = Type.Const(1 as const) + Assert.IsFalse(TypeGuard.IsReadonly(T)) + Assert.IsTrue(TypeGuard.IsLiteral(T)) + Assert.IsEqual(T.const, 1) + }) + it('Should guard for TConst 5', () => { + const T = Type.Const('hello' as const) + Assert.IsFalse(TypeGuard.IsReadonly(T)) + Assert.IsTrue(TypeGuard.IsLiteral(T)) + Assert.IsEqual(T.const, 'hello') + }) + it('Should guard for TConst 6', () => { + const T = Type.Const(true as const) + Assert.IsFalse(TypeGuard.IsReadonly(T)) + Assert.IsTrue(TypeGuard.IsLiteral(T)) + Assert.IsEqual(T.const, true) + }) + // ---------------------------------------------------------------- + // Complex Types + // ---------------------------------------------------------------- + it('Should guard for TConst 7', () => { + const T = Type.Const(100n as const) + Assert.IsFalse(TypeGuard.IsReadonly(T)) + // TS disparity because TLiteral does not support Bigint + Assert.IsTrue(TypeGuard.IsBigInt(T)) + }) + it('Should guard for TConst 8', () => { + const T = Type.Const(new Date()) + Assert.IsFalse(TypeGuard.IsReadonly(T)) + Assert.IsTrue(TypeGuard.IsDate(T)) + }) + it('Should guard for TConst 9', () => { + const T = Type.Const(new Uint8Array()) + Assert.IsFalse(TypeGuard.IsReadonly(T)) + Assert.IsTrue(TypeGuard.IsUint8Array(T)) + }) + it('Should guard for TConst 10', () => { + const T = Type.Const(function () {}) + Assert.IsFalse(TypeGuard.IsReadonly(T)) + Assert.IsTrue(TypeGuard.IsFunction(T)) + Assert.IsTrue(T.parameters.length === 0) + Assert.IsTrue(TypeGuard.IsUnknown(T.returns)) + }) + it('Should guard for TConst 11', () => { + const T = Type.Const(new (class {})()) + Assert.IsFalse(TypeGuard.IsReadonly(T)) + Assert.IsTrue(TypeGuard.IsObject(T)) + // Object types that are neither Date or Uint8Array evaluate as empty objects + Assert.IsEqual(T.properties, {}) + }) + it('Should guard for TConst 12', () => { + const T = Type.Const((function* (): any {})()) + const R = ValueGuard.IsIterator((function* (): any {})()) + Assert.IsFalse(TypeGuard.IsReadonly(T)) + Assert.IsTrue(TypeGuard.IsAny(T)) + }) + it('Should guard for TConst 13', () => { + const T = Type.Const((async function* (): any {})()) + const R = ValueGuard.IsAsyncIterator((function* (): any {})()) + Assert.IsFalse(TypeGuard.IsReadonly(T)) + Assert.IsTrue(TypeGuard.IsAny(T)) + }) + it('Should guard for TConst 14', () => { + const T = Type.Const({ + x: 1, + y: { + z: 2, + }, + } as const) + // root + Assert.IsFalse(TypeGuard.IsReadonly(T)) + Assert.IsTrue(TypeGuard.IsObject(T)) + // x + Assert.IsTrue(TypeGuard.IsLiteral(T.properties.x)) + Assert.IsEqual(T.properties.x.const, 1) + // y + Assert.IsTrue(TypeGuard.IsReadonly(T.properties.y)) + Assert.IsTrue(TypeGuard.IsObject(T.properties.y)) + // y.z + Assert.IsTrue(TypeGuard.IsReadonly(T.properties.y.properties.z)) + Assert.IsTrue(TypeGuard.IsLiteral(T.properties.y.properties.z)) + Assert.IsEqual(T.properties.y.properties.z.const, 2) + }) + it('Should guard for TConst 15', () => { + const T = Type.Const([1, 2, 3] as const) + // root (arrays are always readonly as root) + Assert.IsTrue(TypeGuard.IsReadonly(T)) + Assert.IsTrue(TypeGuard.IsTuple(T)) + Assert.IsTrue(T.items?.length === 3) + // 0 + Assert.IsFalse(TypeGuard.IsReadonly(T.items![0])) + Assert.IsTrue(TypeGuard.IsLiteral(T.items![0])) + // 1 + Assert.IsFalse(TypeGuard.IsReadonly(T.items![1])) + Assert.IsTrue(TypeGuard.IsLiteral(T.items![1])) + // 2 + Assert.IsFalse(TypeGuard.IsReadonly(T.items![2])) + Assert.IsTrue(TypeGuard.IsLiteral(T.items![2])) + }) +}) diff --git a/test/runtime/type/guard/type/constructor.ts b/test/runtime/type/guard/type/constructor.ts new file mode 100644 index 000000000..9c55370ca --- /dev/null +++ b/test/runtime/type/guard/type/constructor.ts @@ -0,0 +1,35 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TConstructor', () => { + it('Should guard for TConstructor', () => { + const R = TypeGuard.IsConstructor(Type.Constructor([], Type.Number())) + Assert.IsTrue(R) + }) + it('Should not guard for TConstructor', () => { + const R = TypeGuard.IsConstructor(null) + Assert.IsFalse(R) + }) + it('Should not guard for TConstructor with invalid $id', () => { + // @ts-ignore + const R = TypeGuard.IsConstructor(Type.Constructor([], Type.Number(), { $id: 1 })) + Assert.IsFalse(R) + }) + it('Should not guard for TConstructor with invalid Params', () => { + const R = TypeGuard.IsConstructor(Type.Constructor([{} as any, {} as any], Type.Number())) + Assert.IsFalse(R) + }) + it('Should not guard for TConstructor with invalid Return', () => { + const R = TypeGuard.IsConstructor(Type.Constructor([], {} as any)) + Assert.IsFalse(R) + }) + it('Should guard for TConstructor with empty Rest Tuple', () => { + const R = TypeGuard.IsConstructor(Type.Constructor(Type.Rest(Type.Tuple([])), Type.Number())) + Assert.IsTrue(R) + }) + it('Should guard for TConstructor with Rest Tuple', () => { + const R = TypeGuard.IsConstructor(Type.Constructor(Type.Rest(Type.Tuple([Type.Number(), Type.String()])), Type.Number())) + Assert.IsTrue(R) + }) +}) diff --git a/test/runtime/type/guard/type/date.ts b/test/runtime/type/guard/type/date.ts new file mode 100644 index 000000000..930cc19af --- /dev/null +++ b/test/runtime/type/guard/type/date.ts @@ -0,0 +1,44 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TDate', () => { + it('Should guard for TDate', () => { + const R = TypeGuard.IsDate(Type.Date()) + Assert.IsTrue(R) + }) + it('Should not guard for TDate', () => { + const R = TypeGuard.IsDate(null) + Assert.IsFalse(R) + }) + it('Should not guard for TDate with invalid $id', () => { + // @ts-ignore + const R = TypeGuard.IsDate(Type.Date({ $id: 1 })) + Assert.IsFalse(R) + }) + it('Should not guard for TDate with invalid exclusiveMaximumTimestamp', () => { + // @ts-ignore + const R = TypeGuard.IsDate(Type.Date({ exclusiveMaximumTimestamp: '1' })) + Assert.IsFalse(R) + }) + it('Should not guard for TDate with invalid exclusiveMinimumTimestamp', () => { + // @ts-ignore + const R = TypeGuard.IsDate(Type.Date({ exclusiveMinimumTimestamp: '1' })) + Assert.IsFalse(R) + }) + it('Should not guard for TDate with invalid maximumTimestamp', () => { + // @ts-ignore + const R = TypeGuard.IsDate(Type.Date({ maximumTimestamp: '1' })) + Assert.IsFalse(R) + }) + it('Should not guard for TDate with invalid minimumTimestamp', () => { + // @ts-ignore + const R = TypeGuard.IsDate(Type.Date({ minimumTimestamp: '1' })) + Assert.IsFalse(R) + }) + it('Should not guard for TDate with invalid multipleOfTimestamp', () => { + // @ts-ignore + const R = TypeGuard.IsDate(Type.Date({ multipleOfTimestamp: '1' })) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/guard/type/enum.ts b/test/runtime/type/guard/type/enum.ts new file mode 100644 index 000000000..4097c1bee --- /dev/null +++ b/test/runtime/type/guard/type/enum.ts @@ -0,0 +1,143 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TEnum', () => { + // ---------------------------------------------------------------- + // Options + // ---------------------------------------------------------------- + it('Should guard for Options 1', () => { + const T = Type.Enum({ x: 1 }, { extra: 'hello', $id: 'T' }) + Assert.IsEqual(T.extra, 'hello') + Assert.IsEqual(T.$id, 'T') + }) + it('Should guard for Options 2', () => { + enum E { + x, + } + const T = Type.Enum(E, { extra: 'hello', $id: 'T' }) + Assert.IsEqual(T.extra, 'hello') + Assert.IsEqual(T.$id, 'T') + }) + it('Should guard for Options 3', () => { + enum E {} + const T = Type.Enum(E, { extra: 'hello', $id: 'T' }) + Assert.IsEqual(T.extra, 'hello') + Assert.IsEqual(T.$id, 'T') + }) + it('Should guard for Options 4', () => { + const T = Type.Enum({}, { extra: 'hello', $id: 'T' }) + Assert.IsEqual(T.extra, 'hello') + Assert.IsEqual(T.$id, 'T') + }) + // ---------------------------------------------------------------- + // Empty + // ---------------------------------------------------------------- + it('Should guard for Empty 1', () => { + const T = Type.Enum({}) + Assert.IsTrue(TypeGuard.IsNever(T)) + }) + it('Should guard for Empty 2', () => { + enum E {} + const T = Type.Enum(E) + Assert.IsTrue(TypeGuard.IsNever(T)) + }) + + // ---------------------------------------------------------------- + // Enum + // ---------------------------------------------------------------- + it('Should guard for TEnum Enum 0', () => { + enum E {} + const T = Type.Enum(E) + Assert.IsTrue(TypeGuard.IsNever(T)) + }) + it('Should guard for TEnum Enum 1', () => { + enum E { + A, + } + const T = Type.Enum(E) + Assert.IsTrue(TypeGuard.IsLiteral(T)) + Assert.IsEqual(T.const, E.A) + }) + it('Should guard for TEnum Enum 2', () => { + enum E { + A = 1, + B = 2, + C = 3, + } + const T = Type.Enum(E) + Assert.IsTrue(TypeGuard.IsUnion(T)) + Assert.IsEqual(T.anyOf[0].const, E.A) + Assert.IsEqual(T.anyOf[1].const, E.B) + Assert.IsEqual(T.anyOf[2].const, E.C) + }) + it('Should guard for TEnum Enum 3', () => { + enum E { + A = 'X', + B = 'Y', + C = 'Z', + } + const T = Type.Enum(E) + Assert.IsTrue(TypeGuard.IsUnion(T)) + Assert.IsEqual(T.anyOf[0].const, E.A) + Assert.IsEqual(T.anyOf[1].const, E.B) + Assert.IsEqual(T.anyOf[2].const, E.C) + }) + it('Should guard for TEnum Enum 4', () => { + enum E { + A = 'X', + B = 'Y', + C = 'X', + } + const T = Type.Enum(E) + Assert.IsTrue(TypeGuard.IsUnion(T)) + Assert.IsEqual(T.anyOf[0].const, E.A) + Assert.IsEqual(T.anyOf[1].const, E.B) + Assert.IsEqual(T.anyOf.length, 2) + }) + // ---------------------------------------------------------------- + // Object Literal + // ---------------------------------------------------------------- + it('Should guard for TEnum Object Literal 0', () => { + const T = Type.Enum({}) + Assert.IsTrue(TypeGuard.IsNever(T)) + }) + it('Should guard for TEnum Object Literal 1', () => { + const T = Type.Enum({ A: 1 }) + Assert.IsTrue(TypeGuard.IsLiteral(T)) + Assert.IsEqual(T.const, 1) + }) + it('Should guard for TEnum Object Literal 2', () => { + const T = Type.Enum({ + A: 1, + B: 2, + C: 3, + }) + Assert.IsTrue(TypeGuard.IsUnion(T)) + Assert.IsEqual(T.anyOf[0].const, 1) + Assert.IsEqual(T.anyOf[1].const, 2) + Assert.IsEqual(T.anyOf[2].const, 3) + }) + it('Should guard for TEnum Object Literal 3', () => { + const T = Type.Enum({ + A: 'X', + B: 'Y', + C: 'Z', + }) + Assert.IsTrue(TypeGuard.IsUnion(T)) + Assert.IsEqual(T.anyOf[0].const, 'X') + Assert.IsEqual(T.anyOf[1].const, 'Y') + Assert.IsEqual(T.anyOf[2].const, 'Z') + }) + it('Should guard for TEnum Object Literal 4', () => { + const T = Type.Enum({ + A: 'X', + B: 'Y', + C: 'X', + }) + Assert.IsTrue(TypeGuard.IsUnion(T)) + Assert.IsEqual(T.anyOf[0].const, 'X') + Assert.IsEqual(T.anyOf[1].const, 'Y') + Assert.IsEqual(T.anyOf.length, 2) + }) +}) diff --git a/test/runtime/type/guard/type/exclude.ts b/test/runtime/type/guard/type/exclude.ts new file mode 100644 index 000000000..973ab850c --- /dev/null +++ b/test/runtime/type/guard/type/exclude.ts @@ -0,0 +1,96 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TExclude', () => { + it('Should exclude string from number', () => { + const T = Type.Exclude(Type.String(), Type.Number()) + Assert.IsTrue(TypeGuard.IsString(T)) + }) + it('Should exclude string from string', () => { + const T = Type.Exclude(Type.String(), Type.String()) + Assert.IsTrue(TypeGuard.IsNever(T)) + }) + it('Should exclude string | number | boolean from string', () => { + const T = Type.Exclude(Type.Union([Type.String(), Type.Number(), Type.Boolean()]), Type.String()) + Assert.IsTrue(TypeGuard.IsUnion(T)) + Assert.IsTrue(TypeGuard.IsNumber(T.anyOf[0])) + Assert.IsTrue(TypeGuard.IsBoolean(T.anyOf[1])) + }) + it('Should exclude string | number | boolean from string | boolean', () => { + const T = Type.Exclude(Type.Union([Type.String(), Type.Number(), Type.Boolean()]), Type.Union([Type.String(), Type.Boolean()])) + Assert.IsTrue(TypeGuard.IsNumber(T)) + }) + // ------------------------------------------------------------------------ + // TemplateLiteral | TemplateLiteral + // ------------------------------------------------------------------------ + it('Should exclude TemplateLiteral | TemplateLiteral 1', () => { + const A = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + const B = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + const T = Type.Exclude(A, B) + Assert.IsTrue(TypeGuard.IsNever(T)) + }) + it('Should exclude TemplateLiteral | TemplateLiteral 1', () => { + const A = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + const B = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B')])]) + const T = Type.Exclude(A, B) + Assert.IsTrue(['C'].includes(T.const)) + }) + it('Should exclude TemplateLiteral | TemplateLiteral 1', () => { + const A = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + const B = Type.TemplateLiteral([Type.Union([Type.Literal('A')])]) + const T = Type.Exclude(A, B) + Assert.IsTrue(['C', 'B'].includes(T.anyOf[0].const)) + Assert.IsTrue(['C', 'B'].includes(T.anyOf[1].const)) + }) + // ------------------------------------------------------------------------ + // TemplateLiteral | Union 1 + // ------------------------------------------------------------------------ + it('Should exclude TemplateLiteral | Union 1', () => { + const A = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + const B = Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')]) + const T = Type.Exclude(A, B) + Assert.IsTrue(TypeGuard.IsNever(T)) + }) + it('Should exclude TemplateLiteral | Union 1', () => { + const A = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + const B = Type.Union([Type.Literal('A'), Type.Literal('B')]) + const T = Type.Exclude(A, B) + Assert.IsTrue(['C'].includes(T.const)) + }) + it('Should exclude TemplateLiteral | Union 1', () => { + const A = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + const B = Type.Union([Type.Literal('A')]) + const T = Type.Exclude(A, B) + Assert.IsTrue(['C', 'B'].includes(T.anyOf[0].const)) + Assert.IsTrue(['C', 'B'].includes(T.anyOf[1].const)) + }) + // ------------------------------------------------------------------------ + // Union | TemplateLiteral 1 + // ------------------------------------------------------------------------ + it('Should exclude Union | TemplateLiteral 1', () => { + const A = Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')]) + const B = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + const T = Type.Exclude(A, B) + Assert.IsTrue(TypeGuard.IsNever(T)) + }) + it('Should exclude Union | TemplateLiteral 1', () => { + const A = Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')]) + const B = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B')])]) + const T = Type.Exclude(A, B) + Assert.IsTrue(['C'].includes(T.const)) + }) + it('Should exclude Union | TemplateLiteral 1', () => { + const A = Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')]) + const B = Type.TemplateLiteral([Type.Union([Type.Literal('A')])]) + const T = Type.Exclude(A, B) + Assert.IsTrue(['C', 'B'].includes(T.anyOf[0].const)) + Assert.IsTrue(['C', 'B'].includes(T.anyOf[1].const)) + }) + it('Should exclude with options', () => { + const A = Type.String() + const B = Type.String() + const T = Type.Exclude(A, B, { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) +}) diff --git a/test/runtime/type/guard/type/extract.ts b/test/runtime/type/guard/type/extract.ts new file mode 100644 index 000000000..93f7b1fda --- /dev/null +++ b/test/runtime/type/guard/type/extract.ts @@ -0,0 +1,102 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TExtract', () => { + it('Should extract string from number', () => { + const T = Type.Extract(Type.String(), Type.Number()) + Assert.IsTrue(TypeGuard.IsNever(T)) + }) + it('Should extract string from string', () => { + const T = Type.Extract(Type.String(), Type.String()) + Assert.IsTrue(TypeGuard.IsString(T)) + }) + it('Should extract string | number | boolean from string', () => { + const T = Type.Extract(Type.Union([Type.String(), Type.Number(), Type.Boolean()]), Type.String()) + Assert.IsTrue(TypeGuard.IsString(T)) + }) + it('Should extract string | number | boolean from string | boolean', () => { + const T = Type.Extract(Type.Union([Type.String(), Type.Number(), Type.Boolean()]), Type.Union([Type.String(), Type.Boolean()])) + Assert.IsTrue(TypeGuard.IsUnion(T)) + Assert.IsTrue(TypeGuard.IsString(T.anyOf[0])) + Assert.IsTrue(TypeGuard.IsBoolean(T.anyOf[1])) + }) + // ------------------------------------------------------------------------ + // TemplateLiteral | TemplateLiteral + // ------------------------------------------------------------------------ + it('Should extract TemplateLiteral | TemplateLiteral 1', () => { + const A = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + const B = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + const T = Type.Extract(A, B) + Assert.IsTrue(['A', 'B', 'C'].includes(T.anyOf[0].const)) + Assert.IsTrue(['A', 'B', 'C'].includes(T.anyOf[1].const)) + Assert.IsTrue(['A', 'B', 'C'].includes(T.anyOf[2].const)) + }) + it('Should extract TemplateLiteral | TemplateLiteral 1', () => { + const A = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + const B = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B')])]) + const T = Type.Extract(A, B) + Assert.IsTrue(['A', 'B'].includes(T.anyOf[0].const)) + Assert.IsTrue(['A', 'B'].includes(T.anyOf[1].const)) + }) + it('Should extract TemplateLiteral | TemplateLiteral 1', () => { + const A = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + const B = Type.TemplateLiteral([Type.Union([Type.Literal('A')])]) + const T = Type.Extract(A, B) + Assert.IsTrue(['A'].includes(T.const)) + }) + // ------------------------------------------------------------------------ + // TemplateLiteral | Union 1 + // ------------------------------------------------------------------------ + it('Should extract TemplateLiteral | Union 1', () => { + const A = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + const B = Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')]) + const T = Type.Extract(A, B) + Assert.IsTrue(['A', 'B', 'C'].includes(T.anyOf[0].const)) + Assert.IsTrue(['A', 'B', 'C'].includes(T.anyOf[1].const)) + Assert.IsTrue(['A', 'B', 'C'].includes(T.anyOf[2].const)) + }) + it('Should extract TemplateLiteral | Union 1', () => { + const A = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + const B = Type.Union([Type.Literal('A'), Type.Literal('B')]) + const T = Type.Extract(A, B) + Assert.IsTrue(['A', 'B'].includes(T.anyOf[0].const)) + Assert.IsTrue(['A', 'B'].includes(T.anyOf[1].const)) + }) + it('Should extract TemplateLiteral | Union 1', () => { + const A = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + const B = Type.Union([Type.Literal('A')]) + const T = Type.Extract(A, B) + Assert.IsTrue(['A'].includes(T.const)) + }) + // ------------------------------------------------------------------------ + // Union | TemplateLiteral 1 + // ------------------------------------------------------------------------ + it('Should extract Union | TemplateLiteral 1', () => { + const A = Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')]) + const B = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + const T = Type.Extract(A, B) + Assert.IsTrue(['A', 'B', 'C'].includes(T.anyOf[0].const)) + Assert.IsTrue(['A', 'B', 'C'].includes(T.anyOf[1].const)) + Assert.IsTrue(['A', 'B', 'C'].includes(T.anyOf[2].const)) + }) + it('Should extract Union | TemplateLiteral 1', () => { + const A = Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')]) + const B = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B')])]) + const T = Type.Extract(A, B) + Assert.IsTrue(['A', 'B'].includes(T.anyOf[0].const)) + Assert.IsTrue(['A', 'B'].includes(T.anyOf[1].const)) + }) + it('Should extract Union | TemplateLiteral 1', () => { + const A = Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')]) + const B = Type.TemplateLiteral([Type.Union([Type.Literal('A')])]) + const T = Type.Extract(A, B) + Assert.IsTrue(['A'].includes(T.const)) + }) + it('Should extract with options', () => { + const A = Type.String() + const B = Type.String() + const T = Type.Extract(A, B, { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) +}) diff --git a/test/runtime/type/guard/type/function.ts b/test/runtime/type/guard/type/function.ts new file mode 100644 index 000000000..81bbfe84c --- /dev/null +++ b/test/runtime/type/guard/type/function.ts @@ -0,0 +1,35 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TFunction', () => { + it('Should guard for TFunction', () => { + const R = TypeGuard.IsFunction(Type.Function([], Type.Number())) + Assert.IsTrue(R) + }) + it('Should not guard for TFunction', () => { + const R = TypeGuard.IsFunction(null) + Assert.IsFalse(R) + }) + it('Should not guard for TFunction with invalid $id', () => { + // @ts-ignore + const R = TypeGuard.IsFunction(Type.Function([], Type.Number(), { $id: 1 })) + Assert.IsFalse(R) + }) + it('Should not guard for TFunction with invalid Params', () => { + const R = TypeGuard.IsFunction(Type.Function([{} as any, {} as any], Type.Number())) + Assert.IsFalse(R) + }) + it('Should not guard for TFunction with invalid Return', () => { + const R = TypeGuard.IsFunction(Type.Function([], {} as any)) + Assert.IsFalse(R) + }) + it('Should guard for TFunction with empty Rest Tuple', () => { + const R = TypeGuard.IsFunction(Type.Function(Type.Rest(Type.Tuple([])), Type.Number())) + Assert.IsTrue(R) + }) + it('Should guard for TFunction with Rest Tuple', () => { + const R = TypeGuard.IsFunction(Type.Function(Type.Rest(Type.Tuple([Type.Number(), Type.String()])), Type.Number())) + Assert.IsTrue(R) + }) +}) diff --git a/test/runtime/type/guard/type/import.ts b/test/runtime/type/guard/type/import.ts new file mode 100644 index 000000000..91080764f --- /dev/null +++ b/test/runtime/type/guard/type/import.ts @@ -0,0 +1,291 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TImport', () => { + it('Should guard for TImport', () => { + const Module = Type.Module({ + A: Type.String(), + }) + const A = Module.Import('A') + const N = A.$defs[A.$ref] + Assert.IsTrue(TypeGuard.IsImport(A)) + Assert.IsTrue(TypeGuard.IsString(N)) + }) + // ---------------------------------------------------------------- + // Computed: Options + // ---------------------------------------------------------------- + it('Should guard for TImport with Options 1', () => { + const Module = Type.Module({ + A: Type.String(), + }) + const A = Module.Import('A', { format: 'string' }) + const N = A.$defs[A.$ref] + Assert.IsTrue(TypeGuard.IsImport(A)) + Assert.IsTrue(TypeGuard.IsString(N)) + Assert.IsTrue(N.format === 'string') + }) + it('Should guard for TImport with Options 2', () => { + const Module = Type.Module({ + R: Type.Object({ x: Type.Number() }), + A: Type.Ref('R'), + }) + const A = Module.Import('A', { test: 'test' }) + const N = A.$defs[A.$ref] + Assert.IsTrue(TypeGuard.IsImport(A)) + Assert.IsTrue(TypeGuard.IsRef(N)) + Assert.IsTrue(N.test === 'test') + }) + it('Should guard for TImport with Options 3', () => { + const Module = Type.Module({ + R: Type.Object({ x: Type.Number() }), + A: Type.Partial(Type.Ref('R')), + }) + const A = Module.Import('A', { additionalProperties: false }) + const N = A.$defs[A.$ref] + Assert.IsTrue(TypeGuard.IsImport(A)) + Assert.IsTrue(TypeGuard.IsObject(N)) + Assert.IsTrue(N.additionalProperties === false) + }) + // ---------------------------------------------------------------- + // Computed: Awaited + // ---------------------------------------------------------------- + it('Should compute for Awaited', () => { + const Module = Type.Module({ + T: Type.Promise(Type.String()), + R: Type.Awaited(Type.Ref('T')), + }) + const T = Module.Import('R') + Assert.IsTrue(TypeGuard.IsString(T.$defs['R'])) + }) + // ---------------------------------------------------------------- + // Computed: Index (Note: Pending Reimplementation of Index) + // ---------------------------------------------------------------- + it('Should compute for Index 2', () => { + const Module = Type.Module({ + T: Type.Object({ x: Type.Number(), y: Type.String() }), + I: Type.Literal('x'), + R: Type.Index(Type.Ref('T'), Type.Ref('I')) as never, // fail + }) + const T = Module.Import('R') + Assert.IsTrue(TypeGuard.IsNumber(T.$defs['R'])) + }) + // ---------------------------------------------------------------- + // Computed: Omit + // ---------------------------------------------------------------- + it('Should compute for Omit 1', () => { + const Module = Type.Module({ + T: Type.Object({ x: Type.Number(), y: Type.Number() }), + R: Type.Omit(Type.Ref('T'), Type.Literal('x')), + }) + const T = Module.Import('R') + Assert.IsTrue(TypeGuard.IsObject(T.$defs['R'])) + Assert.IsTrue(TypeGuard.IsNumber(T.$defs['R'].properties.y)) + // @ts-ignore + Assert.IsTrue(T.$defs['R'].properties.x === undefined) + }) + it('Should compute for Omit 2', () => { + const Module = Type.Module({ + T: Type.Object({ x: Type.Number(), y: Type.Number() }), + K: Type.Literal('x'), + R: Type.Omit(Type.Ref('T'), Type.Ref('K')), + }) + const T = Module.Import('R') + Assert.IsTrue(TypeGuard.IsObject(T.$defs['R'])) + Assert.IsTrue(TypeGuard.IsNumber(T.$defs['R'].properties.y)) + // @ts-ignore + Assert.IsTrue(T.$defs['R'].properties.x === undefined) + }) + // ---------------------------------------------------------------- + // Computed: Partial + // ---------------------------------------------------------------- + it('Should compute for Partial', () => { + const Module = Type.Module({ + T: Type.Object({ x: Type.Number() }), + R: Type.Partial(Type.Ref('T')), + }) + const T = Module.Import('R') + Assert.IsTrue(TypeGuard.IsObject(T.$defs['R'])) + Assert.IsTrue(TypeGuard.IsNumber(T.$defs['R'].properties.x)) + Assert.IsTrue(TypeGuard.IsOptional(T.$defs['R'].properties.x)) + }) + // ---------------------------------------------------------------- + // Computed: Pick + // ---------------------------------------------------------------- + it('Should compute for Pick 1', () => { + const Module = Type.Module({ + T: Type.Object({ x: Type.Number(), y: Type.Number() }), + R: Type.Pick(Type.Ref('T'), Type.Literal('x')), + }) + const T = Module.Import('R') + Assert.IsTrue(TypeGuard.IsObject(T.$defs['R'])) + Assert.IsTrue(TypeGuard.IsNumber(T.$defs['R'].properties.x)) + // @ts-ignore + Assert.IsTrue(T.$defs['R'].properties.y === undefined) + }) + it('Should compute for Pick 2', () => { + const Module = Type.Module({ + T: Type.Object({ x: Type.Number(), y: Type.Number() }), + K: Type.Literal('x'), + R: Type.Pick(Type.Ref('T'), Type.Ref('K')), + }) + const T = Module.Import('R') + Assert.IsTrue(TypeGuard.IsObject(T.$defs['R'])) + Assert.IsTrue(TypeGuard.IsNumber(T.$defs['R'].properties.x)) + // @ts-ignore + Assert.IsTrue(T.$defs['R'].properties.y === undefined) + }) + // ---------------------------------------------------------------- + // Computed: Record + // ---------------------------------------------------------------- + it('Should compute for Record 1', () => { + const Module = Type.Module({ + T: Type.Object({ x: Type.Number(), y: Type.String() }), + R: Type.Record(Type.String(), Type.Ref('T')), + }) + const T = Module.Import('R') + Assert.IsTrue(TypeGuard.IsRecord(T.$defs['R'])) + // note: TRecord> are not computed. Only the Key is + // computed as TypeBox needs to make a deferred call to transform from + // TRecord to TObject for finite keys. + Assert.IsTrue(TypeGuard.IsRef(T.$defs['R'].patternProperties['^(.*)$'])) + Assert.IsTrue(T.$defs['R'].patternProperties['^(.*)$'].$ref === 'T') + }) + it('Should compute for Record 2', () => { + const Module = Type.Module({ + T: Type.Number(), + R: Type.Record(Type.Union([Type.Literal('x'), Type.Literal('y')]), Type.Ref('T')), + }) + // Retain reference if not computed + const T = Module.Import('R') + Assert.IsTrue(TypeGuard.IsObject(T.$defs['R'])) + Assert.IsTrue(TypeGuard.IsRef(T.$defs['R'].properties.x)) + Assert.IsTrue(TypeGuard.IsRef(T.$defs['R'].properties.y)) + }) + it('Should compute for Record 3', () => { + const Module = Type.Module({ + T: Type.Object({ x: Type.Number() }), + R: Type.Record(Type.Union([Type.Literal('x'), Type.Literal('y')]), Type.Partial(Type.Ref('T'))), + }) + // Dereference if computed + const T = Module.Import('R') + Assert.IsTrue(TypeGuard.IsObject(T.$defs['R'])) + Assert.IsTrue(TypeGuard.IsObject(T.$defs['R'].properties.x)) + Assert.IsTrue(TypeGuard.IsObject(T.$defs['R'].properties.y)) + }) + // ---------------------------------------------------------------- + // Computed: Required + // ---------------------------------------------------------------- + it('Should compute for Required', () => { + const Module = Type.Module({ + T: Type.Partial(Type.Object({ x: Type.Number() })), + R: Type.Required(Type.Ref('T')), + }) + const T = Module.Import('R') + Assert.IsTrue(TypeGuard.IsObject(T.$defs['R'])) + Assert.IsTrue(TypeGuard.IsNumber(T.$defs['R'].properties.x)) + Assert.IsFalse(TypeGuard.IsOptional(T.$defs['R'].properties.x)) + }) + // ---------------------------------------------------------------- + // Computed: KeyOf + // ---------------------------------------------------------------- + it('Should compute for KeyOf', () => { + const Module = Type.Module({ + T: Type.Object({ x: Type.Number(), y: Type.String() }), + R: Type.KeyOf(Type.Ref('T')), + }) + const T = Module.Import('R') + Assert.IsTrue(TypeGuard.IsUnion(T.$defs['R'])) + Assert.IsTrue(TypeGuard.IsLiteral(T.$defs['R'].anyOf[0])) + Assert.IsTrue(TypeGuard.IsLiteral(T.$defs['R'].anyOf[1])) + Assert.IsTrue(T.$defs['R'].anyOf[0].const === 'x') + Assert.IsTrue(T.$defs['R'].anyOf[1].const === 'y') + }) + // ---------------------------------------------------------------- + // Modifiers: 1 + // ---------------------------------------------------------------- + it('Should compute for Modifiers 1', () => { + const Module = Type.Module({ + T: Type.Object({ + x: Type.ReadonlyOptional(Type.Null()), + y: Type.Readonly(Type.Null()), + z: Type.Optional(Type.Null()), + w: Type.Null(), + }), + }) + const T = Module.Import('T') + const R = T.$defs[T.$ref] + Assert.IsTrue(TypeGuard.IsObject(R)) + + Assert.IsTrue(TypeGuard.IsNull(R.properties.x)) + Assert.IsTrue(TypeGuard.IsReadonly(R.properties.x)) + Assert.IsTrue(TypeGuard.IsOptional(R.properties.x)) + + Assert.IsTrue(TypeGuard.IsNull(R.properties.y)) + Assert.IsTrue(TypeGuard.IsReadonly(R.properties.y)) + Assert.IsFalse(TypeGuard.IsOptional(R.properties.y)) + + Assert.IsTrue(TypeGuard.IsNull(R.properties.z)) + Assert.IsTrue(TypeGuard.IsOptional(R.properties.z)) + Assert.IsFalse(TypeGuard.IsReadonly(R.properties.z)) + + Assert.IsTrue(TypeGuard.IsNull(R.properties.w)) + Assert.IsFalse(TypeGuard.IsOptional(R.properties.w)) + Assert.IsFalse(TypeGuard.IsReadonly(R.properties.w)) + }) + // ---------------------------------------------------------------- + // Modifiers: 2 + // ---------------------------------------------------------------- + it('Should compute for Modifiers 2', () => { + const Module = Type.Module({ + T: Type.Object({ + x: Type.ReadonlyOptional(Type.Array(Type.Null())), + y: Type.Readonly(Type.Array(Type.Null())), + z: Type.Optional(Type.Array(Type.Null())), + w: Type.Array(Type.Null()), + }), + }) + const T = Module.Import('T') + const R = T.$defs[T.$ref] + Assert.IsTrue(TypeGuard.IsObject(R)) + + Assert.IsTrue(TypeGuard.IsArray(R.properties.x)) + Assert.IsTrue(TypeGuard.IsNull(R.properties.x.items)) + Assert.IsTrue(TypeGuard.IsReadonly(R.properties.x)) + Assert.IsTrue(TypeGuard.IsOptional(R.properties.x)) + + Assert.IsTrue(TypeGuard.IsArray(R.properties.y)) + Assert.IsTrue(TypeGuard.IsNull(R.properties.y.items)) + Assert.IsTrue(TypeGuard.IsReadonly(R.properties.y)) + Assert.IsFalse(TypeGuard.IsOptional(R.properties.y)) + + Assert.IsTrue(TypeGuard.IsArray(R.properties.z)) + Assert.IsTrue(TypeGuard.IsNull(R.properties.z.items)) + Assert.IsTrue(TypeGuard.IsOptional(R.properties.z)) + Assert.IsFalse(TypeGuard.IsReadonly(R.properties.z)) + + Assert.IsTrue(TypeGuard.IsArray(R.properties.w)) + Assert.IsTrue(TypeGuard.IsNull(R.properties.w.items)) + Assert.IsFalse(TypeGuard.IsOptional(R.properties.w)) + Assert.IsFalse(TypeGuard.IsReadonly(R.properties.w)) + }) + // ---------------------------------------------------------------- + // Modifiers: 3 + // ---------------------------------------------------------------- + it('Should compute for Modifiers 3', () => { + const Module = Type.Module({ + T: Type.Object({ + x: Type.Array(Type.Null()), + }), + // Computed Partial + U: Type.Partial(Type.Ref('T')), + }) + const T = Module.Import('U') + const R = T.$defs[T.$ref] + Assert.IsTrue(TypeGuard.IsObject(R)) + + Assert.IsTrue(TypeGuard.IsArray(R.properties.x)) + Assert.IsTrue(TypeGuard.IsNull(R.properties.x.items)) + Assert.IsTrue(TypeGuard.IsOptional(R.properties.x)) + }) +}) diff --git a/test/runtime/type/guard/type/index.ts b/test/runtime/type/guard/type/index.ts new file mode 100644 index 000000000..d8e59fb7d --- /dev/null +++ b/test/runtime/type/guard/type/index.ts @@ -0,0 +1,54 @@ +import './any' +import './argument' +import './array' +import './async-iterator' +import './awaited' +import './bigint' +import './boolean' +import './capitalize' +import './composite' +import './computed' +import './const' +import './constructor' +import './date' +import './enum' +import './exclude' +import './extract' +import './function' +import './import' +import './indexed' +import './integer' +import './intersect' +import './iterator' +import './keyof' +import './kind' +import './literal' +import './lowercase' +import './mapped' +import './not' +import './null' +import './number' +import './object' +import './omit' +import './partial' +import './pick' +import './promise' +import './record' +import './recursive' +import './ref' +import './regexp' +import './required' +import './rest' +import './string' +import './symbol' +import './template-literal' +import './this' +import './tuple' +import './uint8array' +import './uncapitalize' +import './undefined' +import './union' +import './unknown' +import './unsafe' +import './uppercase' +import './void' diff --git a/test/runtime/type/guard/type/indexed.ts b/test/runtime/type/guard/type/indexed.ts new file mode 100644 index 000000000..55225c894 --- /dev/null +++ b/test/runtime/type/guard/type/indexed.ts @@ -0,0 +1,327 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TIndex', () => { + it('Should Index 1', () => { + const T = Type.Object({ + x: Type.Number(), + y: Type.String(), + }) + const I = Type.Index(T, ['x']) + Assert.IsTrue(TypeGuard.IsNumber(I)) + }) + it('Should Index 2', () => { + const T = Type.Object({ + x: Type.Number(), + y: Type.String(), + }) + const I = Type.Index(T, ['x', 'y']) + Assert.IsTrue(TypeGuard.IsUnion(I)) + Assert.IsTrue(TypeGuard.IsNumber(I.anyOf[0])) + Assert.IsTrue(TypeGuard.IsString(I.anyOf[1])) + }) + it('Should Index 3', () => { + const T = Type.Object({ + x: Type.Number(), + y: Type.String(), + }) + const I = Type.Index(T, Type.KeyOf(T)) + Assert.IsTrue(TypeGuard.IsUnion(I)) + Assert.IsTrue(TypeGuard.IsNumber(I.anyOf[0])) + Assert.IsTrue(TypeGuard.IsString(I.anyOf[1])) + }) + it('Should Index 4', () => { + const T = Type.Object({ + ab: Type.Number(), + ac: Type.String(), + }) + const I = Type.Index(T, Type.TemplateLiteral([Type.Literal('a'), Type.Union([Type.Literal('b'), Type.Literal('c')])])) + Assert.IsTrue(TypeGuard.IsUnion(I)) + Assert.IsTrue(TypeGuard.IsNumber(I.anyOf[0])) + Assert.IsTrue(TypeGuard.IsString(I.anyOf[1])) + }) + it('Should Index 5', () => { + const T = Type.Intersect([Type.Object({ x: Type.Number() }), Type.Object({ y: Type.String() })]) + const I = Type.Index(T, ['x', 'y']) + Assert.IsTrue(TypeGuard.IsUnion(I)) + Assert.IsTrue(TypeGuard.IsNumber(I.anyOf[0])) + Assert.IsTrue(TypeGuard.IsString(I.anyOf[1])) + }) + it('Should Index 6', () => { + const T = Type.Union([Type.Object({ x: Type.Number() }), Type.Object({ x: Type.String() })]) + const I = Type.Index(T, ['x']) + Assert.IsTrue(TypeGuard.IsUnion(I)) + Assert.IsTrue(TypeGuard.IsNumber(I.anyOf[0])) + Assert.IsTrue(TypeGuard.IsString(I.anyOf[1])) + }) + it('Should Index 7', () => { + const T = Type.Array(Type.Null()) + const I = Type.Index(T, Type.Number()) + Assert.IsTrue(TypeGuard.IsNull(I)) + }) + it('Should Index 6', () => { + const T = Type.Tuple([Type.Literal('hello'), Type.Literal('world')]) + const I = Type.Index(T, [0]) + Assert.IsTrue(TypeGuard.IsLiteralString(I)) + Assert.IsEqual(I.const, 'hello') + }) + it('Should Index 8', () => { + const T = Type.Tuple([Type.Literal('hello'), Type.Literal('world')]) + const I = Type.Index(T, [1]) + Assert.IsTrue(TypeGuard.IsLiteralString(I)) + Assert.IsEqual(I.const, 'world') + }) + it('Should Index 9', () => { + const T = Type.Tuple([Type.Literal('hello'), Type.Literal('world')]) + const I = Type.Index(T, [0, 1]) + Assert.IsTrue(TypeGuard.IsUnion(I)) + Assert.IsEqual(I.anyOf[0].const, 'hello') + Assert.IsEqual(I.anyOf[1].const, 'world') + }) + it('Should Index 10', () => { + const T = Type.Tuple([Type.Literal('hello'), Type.Literal('world')]) + const I = Type.Index(T, [1, 0]) + Assert.IsTrue(TypeGuard.IsUnion(I)) + Assert.IsEqual(I.anyOf[0].const, 'world') + Assert.IsEqual(I.anyOf[1].const, 'hello') + }) + it('Should Index 11', () => { + const T = Type.Tuple([Type.Literal('hello'), Type.Literal('world')]) + const I = Type.Index(T, [0, 0, 0, 1]) + Assert.IsTrue(TypeGuard.IsUnion(I)) + Assert.IsEqual(I.anyOf[0].const, 'hello') + Assert.IsEqual(I.anyOf[1].const, 'hello') + Assert.IsEqual(I.anyOf[2].const, 'hello') + Assert.IsEqual(I.anyOf[3].const, 'world') + }) + it('Should Index 12', () => { + const T = Type.Tuple([Type.String(), Type.Boolean()]) + const I = Type.Index(T, Type.Number()) + Assert.IsTrue(TypeGuard.IsUnion(I)) + Assert.IsTrue(TypeGuard.IsString(I.anyOf[0])) + Assert.IsTrue(TypeGuard.IsBoolean(I.anyOf[1])) + }) + it('Should Index 13', () => { + const T = Type.Tuple([Type.String()]) + const I = Type.Index(T, Type.Number()) + Assert.IsTrue(TypeGuard.IsString(I)) + }) + it('Should Index 14', () => { + const T = Type.Tuple([]) + const I = Type.Index(T, Type.Number()) + Assert.IsTrue(TypeGuard.IsNever(I)) + }) + it('Should Index 15', () => { + const T = Type.Object({ + 0: Type.Number(), + }) + const I = Type.Index(T, Type.Literal(0)) + Assert.IsTrue(TypeGuard.IsNumber(I)) + }) + it('Should Index 16', () => { + const T = Type.Object({ + 0: Type.Number(), + }) + const I = Type.Index(T, Type.Literal('0')) + Assert.IsTrue(TypeGuard.IsNumber(I)) + }) + it('Should Index 17', () => { + const T = Type.Object({ + '0': Type.Number(), + }) + const I = Type.Index(T, Type.Literal(0)) + Assert.IsTrue(TypeGuard.IsNumber(I)) + }) + it('Should Index 18', () => { + const T = Type.Object({ + '0': Type.Number(), + }) + const I = Type.Index(T, Type.Literal('0')) + Assert.IsTrue(TypeGuard.IsNumber(I)) + }) + it('Should Index 19', () => { + const T = Type.Object({ + 0: Type.Number(), + 1: Type.String(), + 2: Type.Boolean(), + }) + const I = Type.Index(T, Type.Union([Type.Literal(0), Type.Literal(2)])) + Assert.IsTrue(TypeGuard.IsUnion(I)) + Assert.IsTrue(TypeGuard.IsNumber(I.anyOf[0])) + Assert.IsTrue(TypeGuard.IsBoolean(I.anyOf[1])) + }) + it('Should Index 20', () => { + const T = Type.Object({ + 0: Type.Number(), + 1: Type.String(), + 2: Type.Boolean(), + }) + const I = Type.Index(T, Type.BigInt()) + Assert.IsTrue(TypeGuard.IsNever(I)) + }) + it('Should Index 21', () => { + const T = Type.Object({ + 0: Type.Number(), + 1: Type.String(), + 2: Type.Boolean(), + }) + const I = Type.Index(T, Type.Object({})) + Assert.IsTrue(TypeGuard.IsNever(I)) + }) + it('Should Index 22', () => { + const A = Type.Object({ x: Type.Literal('A') }) + const B = Type.Object({ x: Type.Literal('B') }) + const C = Type.Object({ x: Type.Literal('C') }) + const D = Type.Object({ x: Type.Literal('D') }) + const T = Type.Intersect([A, B, C, D]) + const I = Type.Index(T, ['x']) + Assert.IsTrue(TypeGuard.IsIntersect(I)) + Assert.IsTrue(TypeGuard.IsLiteral(I.allOf[0])) + Assert.IsTrue(TypeGuard.IsLiteral(I.allOf[1])) + Assert.IsTrue(TypeGuard.IsLiteral(I.allOf[2])) + Assert.IsTrue(TypeGuard.IsLiteral(I.allOf[3])) + }) + it('Should Index 23', () => { + const A = Type.Object({ x: Type.Literal('A') }) + const B = Type.Object({ x: Type.Literal('B') }) + const C = Type.Object({ x: Type.Literal('C') }) + const D = Type.Object({ x: Type.Literal('D') }) + const T = Type.Union([A, B, C, D]) + const I = Type.Index(T, ['x']) + Assert.IsTrue(TypeGuard.IsUnion(I)) + Assert.IsTrue(TypeGuard.IsLiteral(I.anyOf[0])) + Assert.IsTrue(TypeGuard.IsLiteral(I.anyOf[1])) + Assert.IsTrue(TypeGuard.IsLiteral(I.anyOf[2])) + Assert.IsTrue(TypeGuard.IsLiteral(I.anyOf[3])) + }) + it('Should Index 24', () => { + const A = Type.Object({ x: Type.Literal('A'), y: Type.Number() }) + const B = Type.Object({ x: Type.Literal('B') }) + const C = Type.Object({ x: Type.Literal('C') }) + const D = Type.Object({ x: Type.Literal('D') }) + const T = Type.Intersect([A, B, C, D]) + const I = Type.Index(T, ['x', 'y']) + Assert.IsTrue(TypeGuard.IsUnion(I)) + Assert.IsTrue(TypeGuard.IsIntersect(I.anyOf[0])) + Assert.IsTrue(TypeGuard.IsNumber(I.anyOf[1])) + }) + it('Should Index 25', () => { + const A = Type.Object({ x: Type.Literal('A'), y: Type.Number() }) + const B = Type.Object({ x: Type.Literal('B'), y: Type.String() }) + const C = Type.Object({ x: Type.Literal('C') }) + const D = Type.Object({ x: Type.Literal('D') }) + const T = Type.Intersect([A, B, C, D]) + const I = Type.Index(T, ['x', 'y']) + Assert.IsTrue(TypeGuard.IsUnion(I)) + Assert.IsTrue(TypeGuard.IsIntersect(I.anyOf[0])) + Assert.IsTrue(TypeGuard.IsIntersect(I.anyOf[1])) + Assert.IsTrue(TypeGuard.IsNumber(I.anyOf[1].allOf[0])) + Assert.IsTrue(TypeGuard.IsString(I.anyOf[1].allOf[1])) + }) + it('Should Index 26', () => { + const T = Type.Recursive((This) => + Type.Object({ + x: Type.String(), + y: Type.Number(), + z: This, + }), + ) + const I = Type.Index(T, ['x', 'y', 'z']) + Assert.IsTrue(TypeGuard.IsUnion(I)) + Assert.IsTrue(TypeGuard.IsString(I.anyOf[0])) + Assert.IsTrue(TypeGuard.IsNumber(I.anyOf[1])) + Assert.IsTrue(TypeGuard.IsThis(I.anyOf[2])) + }) + it('Should Index 27', () => { + const T = Type.Object({ + 0: Type.String(), + 1: Type.Number(), + }) + const I = Type.Index(T, [0, 1]) + Assert.IsTrue(TypeGuard.IsUnion(I)) + Assert.IsTrue(TypeGuard.IsString(I.anyOf[0])) + Assert.IsTrue(TypeGuard.IsNumber(I.anyOf[1])) + }) + it('Should Index 28', () => { + const T = Type.Object({ + 0: Type.String(), + '1': Type.Number(), + }) + const I = Type.Index(T, [0, '1']) + Assert.IsTrue(TypeGuard.IsUnion(I)) + Assert.IsTrue(TypeGuard.IsString(I.anyOf[0])) + Assert.IsTrue(TypeGuard.IsNumber(I.anyOf[1])) + }) + it('Should Index 29', () => { + const T = Type.Object({ + 0: Type.String(), + '1': Type.Number(), + }) + const I = Type.Index(T, Type.Union([Type.Literal(0), Type.Literal('1')])) + Assert.IsTrue(TypeGuard.IsUnion(I)) + Assert.IsTrue(TypeGuard.IsString(I.anyOf[0])) + Assert.IsTrue(TypeGuard.IsNumber(I.anyOf[1])) + }) + it('Should Index 30', () => { + const T = Type.Object({ + 0: Type.String(), + '1': Type.Number(), + }) + const I = Type.Index(T, Type.Union([Type.Literal(0), Type.Literal(1)])) + Assert.IsTrue(TypeGuard.IsUnion(I)) + Assert.IsTrue(TypeGuard.IsString(I.anyOf[0])) + Assert.IsTrue(TypeGuard.IsNumber(I.anyOf[1])) + // Note: Expect TNever for anyOf[1] but permit for TNumber due to IndexedAccess + // Resolve() which currently cannot differentiate between string and numeric keys + // on the object. This may be resolvable in later revisions, but test for this + // fall-through to ensure case is documented. For review. + }) + // -------------------------------------------------------- + // Modifier Optional Indexing + // -------------------------------------------------------- + it('Should Index 31', () => { + const T = Type.Object({ + x: Type.Optional(Type.String()), + y: Type.Number(), + }) + const I = Type.Index(T, ['x']) + Assert.IsTrue(TypeGuard.IsOptional(I)) + Assert.IsTrue(TypeGuard.IsString(I)) + }) + it('Should Index 32', () => { + const T = Type.Object({ + x: Type.Optional(Type.String()), + y: Type.Number(), + }) + const I = Type.Index(T, ['y']) + Assert.IsFalse(TypeGuard.IsOptional(I)) + Assert.IsTrue(TypeGuard.IsNumber(I)) + }) + it('Should Index 33', () => { + const T = Type.Object({ + x: Type.Optional(Type.String()), + y: Type.Number(), + }) + const I = Type.Index(T, ['x', 'y']) + Assert.IsTrue(TypeGuard.IsOptional(I)) + Assert.IsTrue(TypeGuard.IsUnion(I)) + Assert.IsTrue(TypeGuard.IsString(I.anyOf[0])) + Assert.IsTrue(TypeGuard.IsNumber(I.anyOf[1])) + }) + it('Should Index 34', () => { + const T = Type.String() + const I = Type.Index(T, ['x']) + Assert.IsTrue(TypeGuard.IsNever(I)) + }) + it('Should Index 35', () => { + const T = Type.Array(Type.String()) + const I = Type.Index(T, Type.Number()) + Assert.IsTrue(TypeGuard.IsString(I)) + }) + it('Should Index 36', () => { + const T = Type.Array(Type.String()) + const I = Type.Index(T, ['[number]']) + Assert.IsTrue(TypeGuard.IsString(I)) + }) +}) diff --git a/test/runtime/type/guard/type/integer.ts b/test/runtime/type/guard/type/integer.ts new file mode 100644 index 000000000..add1f318c --- /dev/null +++ b/test/runtime/type/guard/type/integer.ts @@ -0,0 +1,44 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TInteger', () => { + it('Should guard for TInteger', () => { + const R = TypeGuard.IsInteger(Type.Integer()) + Assert.IsTrue(R) + }) + it('Should not guard for TInteger', () => { + const R = TypeGuard.IsInteger(null) + Assert.IsFalse(R) + }) + it('Should not guard for TInteger with invalid $id', () => { + // @ts-ignore + const R = TypeGuard.IsInteger(Type.Integer({ $id: 1 })) + Assert.IsFalse(R) + }) + it('Should not guard for TInteger with invalid multipleOf', () => { + // @ts-ignore + const R = TypeGuard.IsInteger(Type.Integer({ multipleOf: '1' })) + Assert.IsFalse(R) + }) + it('Should not guard for TInteger with invalid minimum', () => { + // @ts-ignore + const R = TypeGuard.IsInteger(Type.Integer({ minimum: '1' })) + Assert.IsFalse(R) + }) + it('Should not guard for TInteger with invalid maximum', () => { + // @ts-ignore + const R = TypeGuard.IsInteger(Type.Integer({ maximum: '1' })) + Assert.IsFalse(R) + }) + it('Should not guard for TInteger with invalid exclusiveMinimum', () => { + // @ts-ignore + const R = TypeGuard.IsInteger(Type.Integer({ exclusiveMinimum: '1' })) + Assert.IsFalse(R) + }) + it('Should not guard for TInteger with invalid exclusiveMaximum', () => { + // @ts-ignore + const R = TypeGuard.IsInteger(Type.Integer({ exclusiveMaximum: '1' })) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/guard/type/intersect.ts b/test/runtime/type/guard/type/intersect.ts new file mode 100644 index 000000000..7b345ef66 --- /dev/null +++ b/test/runtime/type/guard/type/intersect.ts @@ -0,0 +1,39 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TIntersect', () => { + it('Should guard for TIntersect', () => { + const R = TypeGuard.IsIntersect( + Type.Intersect([ + Type.Object({ + x: Type.Number(), + }), + Type.Object({ + y: Type.Number(), + }), + ]), + ) + Assert.IsTrue(R) + }) + it('Should not guard for TIntersect', () => { + const R = TypeGuard.IsIntersect( + Type.Union([ + Type.Object({ + x: Type.Number(), + }), + Type.Object({ + y: Type.Number(), + }), + ]), + ) + Assert.IsFalse(R) + }) + it('Should throw for intersected transform types', () => { + const N = Type.Transform(Type.Number()) + .Decode((value) => value) + .Encode((value) => value) + + Assert.Throws(() => Type.Intersect([N, N])) + }) +}) diff --git a/test/runtime/type/guard/type/iterator.ts b/test/runtime/type/guard/type/iterator.ts new file mode 100644 index 000000000..74e984c49 --- /dev/null +++ b/test/runtime/type/guard/type/iterator.ts @@ -0,0 +1,22 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TIterator', () => { + it('Should guard for TIterator', () => { + const T = Type.Iterator(Type.Any()) + const R = TypeGuard.IsIterator(T) + Assert.IsTrue(R) + }) + it('Should not guard for TIterator', () => { + const T = null + const R = TypeGuard.IsIterator(T) + Assert.IsFalse(R) + }) + it('Should not guard for TIterator with invalid $id', () => { + //@ts-ignore + const T = Type.Iterator(Type.Any(), { $id: 1 }) + const R = TypeGuard.IsIterator(T) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/guard/type/keyof.ts b/test/runtime/type/guard/type/keyof.ts new file mode 100644 index 000000000..50d95ec5a --- /dev/null +++ b/test/runtime/type/guard/type/keyof.ts @@ -0,0 +1,76 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TKeyOf', () => { + it('Should KeyOf 1', () => { + const T = Type.Object({ + x: Type.Number(), + y: Type.Number(), + }) + const K = Type.KeyOf(T) + Assert.IsTrue(TypeGuard.IsUnion(K)) + Assert.IsTrue(TypeGuard.IsLiteral(K.anyOf[0])) + Assert.IsTrue(TypeGuard.IsLiteral(K.anyOf[1])) + }) + it('Should KeyOf 2', () => { + const T = Type.Recursive((Self) => + Type.Object({ + x: Type.Number(), + y: Type.Array(Self), + }), + ) + const K = Type.KeyOf(T) + Assert.IsTrue(TypeGuard.IsUnion(K)) + Assert.IsTrue(TypeGuard.IsLiteral(K.anyOf[0])) + Assert.IsTrue(TypeGuard.IsLiteral(K.anyOf[1])) + }) + it('Should KeyOf 3', () => { + const T = Type.Intersect([ + Type.Object({ + x: Type.Number(), + }), + Type.Object({ + y: Type.Number(), + }), + ]) + const K = Type.KeyOf(T) + Assert.IsTrue(TypeGuard.IsUnion(K)) + Assert.IsTrue(TypeGuard.IsLiteral(K.anyOf[0])) + Assert.IsTrue(TypeGuard.IsLiteral(K.anyOf[1])) + }) + it('Should KeyOf 4', () => { + const T = Type.Union([ + Type.Object({ + x: Type.Number(), + }), + Type.Object({ + y: Type.Number(), + }), + ]) + const K = Type.KeyOf(T) + Assert.IsTrue(TypeGuard.IsNever(K)) + }) + it('Should KeyOf 5', () => { + const T = Type.Null() + const K = Type.KeyOf(T) + Assert.IsTrue(TypeGuard.IsNever(K)) + }) + it('Should KeyOf 6', () => { + const T = Type.Array(Type.Number()) + const K = Type.KeyOf(T) + Assert.IsTrue(TypeGuard.IsNumber(K)) + }) + it('Should KeyOf 7', () => { + const T = Type.Tuple([]) + const K = Type.KeyOf(T) + Assert.IsTrue(TypeGuard.IsNever(K)) + }) + it('Should KeyOf 8', () => { + const T = Type.Tuple([Type.Number(), Type.Null()]) + const K = Type.KeyOf(T) + Assert.IsTrue(TypeGuard.IsUnion(K)) + Assert.IsEqual(K.anyOf[0].const, '0') + Assert.IsEqual(K.anyOf[1].const, '1') + }) +}) diff --git a/test/runtime/type/guard/type/kind.ts b/test/runtime/type/guard/type/kind.ts new file mode 100644 index 000000000..f0223e8ad --- /dev/null +++ b/test/runtime/type/guard/type/kind.ts @@ -0,0 +1,13 @@ +import { TypeGuard, Kind } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TKind', () => { + it('Should guard 1', () => { + const T = { [Kind]: 'Kind' } + Assert.IsTrue(TypeGuard.IsKind(T)) + }) + it('Should guard 2', () => { + const T = {} + Assert.IsFalse(TypeGuard.IsKind(T)) + }) +}) diff --git a/test/runtime/type/guard/type/literal.ts b/test/runtime/type/guard/type/literal.ts new file mode 100644 index 000000000..fcfe4b74e --- /dev/null +++ b/test/runtime/type/guard/type/literal.ts @@ -0,0 +1,32 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TLiteral', () => { + it('Should guard for TLiteral of String', () => { + const R = TypeGuard.IsLiteral(Type.Literal('hello')) + Assert.IsTrue(R) + }) + it('Should guard for TLiteral of Number', () => { + const R = TypeGuard.IsLiteral(Type.Literal(42)) + Assert.IsTrue(R) + }) + it('Should guard for TLiteral of Boolean', () => { + const R = TypeGuard.IsLiteral(Type.Literal(true)) + Assert.IsTrue(R) + }) + it('Should not guard for TLiteral of Null', () => { + // @ts-ignore + const R = TypeGuard.IsLiteral(Type.Literal(null)) + Assert.IsFalse(R) + }) + it('Should not guard for TLiteral', () => { + const R = TypeGuard.IsLiteral(null) + Assert.IsFalse(R) + }) + it('Should not guard for TLiteral with invalid $id', () => { + // @ts-ignore + const R = TypeGuard.IsLiteral(Type.Literal(42, { $id: 1 })) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/guard/type/lowercase.ts b/test/runtime/type/guard/type/lowercase.ts new file mode 100644 index 000000000..f274be4b4 --- /dev/null +++ b/test/runtime/type/guard/type/lowercase.ts @@ -0,0 +1,33 @@ +import { TypeGuard, Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/Lowercase', () => { + it('Should guard for Lowercase 1', () => { + const T = Type.Lowercase(Type.Literal('HELLO'), { $id: 'hello', foo: 1 }) + Assert.IsTrue(TypeGuard.IsLiteral(T)) + Assert.IsEqual(T.const, 'hello') + Assert.IsEqual(T.$id, 'hello') + Assert.IsEqual(T.foo, 1) + }) + it('Should guard for Lowercase 2', () => { + const T = Type.Lowercase(Type.Literal('HELLO')) + Assert.IsTrue(TypeGuard.IsLiteral(T)) + Assert.IsEqual(T.const, 'hello') + }) + it('Should guard for Lowercase 3', () => { + const T = Type.Lowercase(Type.Union([Type.Literal('HELLO'), Type.Literal('WORLD')])) + Assert.IsTrue(TypeGuard.IsUnion(T)) + Assert.IsEqual(T.anyOf[0].const, 'hello') + Assert.IsEqual(T.anyOf[1].const, 'world') + }) + it('Should guard for Lowercase 4', () => { + const T = Type.Lowercase(Type.TemplateLiteral('HELLO${0|1}')) + Assert.IsTrue(TypeGuard.IsTemplateLiteral(T)) + Assert.IsEqual(T.pattern, '^(hello0|hello1)$') + }) + it('Should guard for Lowercase 5', () => { + const T = Type.Lowercase(Type.TemplateLiteral([Type.Literal('HELLO'), Type.Union([Type.Literal(0), Type.Literal(1)])])) + Assert.IsTrue(TypeGuard.IsTemplateLiteral(T)) + Assert.IsEqual(T.pattern, '^(hello0|hello1)$') + }) +}) diff --git a/test/runtime/type/guard/type/mapped.ts b/test/runtime/type/guard/type/mapped.ts new file mode 100644 index 000000000..99eeff52b --- /dev/null +++ b/test/runtime/type/guard/type/mapped.ts @@ -0,0 +1,609 @@ +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +// prettier-ignore +describe('guard/type/Mapped', () => { + it('Should guard mapped 1', () => { + const T = Type.Mapped(Type.Union([ + Type.Literal('x'), + Type.Literal('y'), + Type.Literal('z'), + ]), _ => Type.Number(), { custom: 1 }) + Assert.IsEqual(T, Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number() + }, { custom: 1 })) + }) + it('Should guard mapped 2', () => { + const T = Type.Mapped(Type.Union([ + Type.Literal('x'), + Type.Literal('y'), + Type.Literal('z'), + ]), _ => Type.Number()) + Assert.IsEqual(T, Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number() + })) + }) + it('Should guard mapped 3', () => { + const T = Type.Mapped(Type.Union([ + Type.Literal('x'), + Type.Literal('y'), + Type.Literal('z'), + ]), K => K) + Assert.IsEqual(T, Type.Object({ + x: Type.Literal('x'), + y: Type.Literal('y'), + z: Type.Literal('z'), + })) + }) + it('Should guard mapped 4', () => { + const T = Type.Mapped(Type.TemplateLiteral('${0|1}${0|1}'), K => Type.Number()) + Assert.IsEqual(T, Type.Object({ + '00': Type.Number(), + '01': Type.Number(), + '10': Type.Number(), + '11': Type.Number(), + })) + }) + it('Should guard mapped 5', () => { + const T = Type.Mapped(Type.TemplateLiteral('${a|b}'), X => + Type.Mapped(Type.TemplateLiteral('${c|d}'), Y => + Type.Mapped(Type.TemplateLiteral('${e|f}'), Z => + Type.Tuple([X, Y, Z]) + ) + ) + ) + Assert.IsEqual(T, Type.Object({ + a: Type.Object({ + c: Type.Object({ + e: Type.Tuple([Type.Literal("a"), Type.Literal("c"), Type.Literal("e")]), + f: Type.Tuple([Type.Literal("a"), Type.Literal("c"), Type.Literal("f")]) + }), + d: Type.Object({ + e: Type.Tuple([Type.Literal("a"), Type.Literal("d"), Type.Literal("e")]), + f: Type.Tuple([Type.Literal("a"), Type.Literal("d"), Type.Literal("f")]) + }), + }), + b: Type.Object({ + c: Type.Object({ + e: Type.Tuple([Type.Literal("b"), Type.Literal("c"), Type.Literal("e")]), + f: Type.Tuple([Type.Literal("b"), Type.Literal("c"), Type.Literal("f")]) + }), + d: Type.Object({ + e: Type.Tuple([Type.Literal("b"), Type.Literal("d"), Type.Literal("e")]), + f: Type.Tuple([Type.Literal("b"), Type.Literal("d"), Type.Literal("f")]) + }), + }), + })) + }) + it('Should guard mapped 6', () => { + const T = Type.Object({ + x: Type.Number(), + y: Type.String(), + z: Type.Boolean() + }) + const M = Type.Mapped(Type.KeyOf(T), K => K) + Assert.IsEqual(M, Type.Object({ + x: Type.Literal('x'), + y: Type.Literal('y'), + z: Type.Literal('z') + })) + }) + it('Should guard mapped 7', () => { + const T = Type.Object({ + x: Type.Number(), + y: Type.String(), + z: Type.Boolean() + }) + const M = Type.Mapped(Type.KeyOf(T), K => Type.Index(T, K)) + Assert.IsEqual(M, Type.Object({ + x: Type.Number(), + y: Type.String(), + z: Type.Boolean() + })) + }) + it('Should guard mapped 8', () => { + const T = Type.Object({ + x: Type.Number(), + y: Type.String(), + z: Type.Boolean() + }) + const M = Type.Mapped(Type.KeyOf(T), K => Type.Index(T, K, { custom: 1 })) + Assert.IsEqual(M, Type.Object({ + x: Type.Number({ custom: 1 }), + y: Type.String({ custom: 1 }), + z: Type.Boolean({ custom: 1 }) + })) + }) + // ---------------------------------------------------------------- + // Extract + // ---------------------------------------------------------------- + it('Should guard mapped 9', () => { + const T = Type.Object({ + x: Type.Union([Type.String(), Type.Number(), Type.Boolean()]) + }) + const M = Type.Mapped(Type.KeyOf(T), K => { + return Type.Extract(Type.Index(T, K), Type.String()) + }) + Assert.IsEqual(M, Type.Object({ + x: Type.String() + })) + }) + it('Should guard mapped 10', () => { + const T = Type.Object({ + x: Type.Union([Type.String(), Type.Number(), Type.Boolean()]) + }) + const M = Type.Mapped(Type.KeyOf(T), K => { + return Type.Extract(Type.Index(T, K), Type.Union([ + Type.String(), + Type.Number() + ])) + }) + Assert.IsEqual(M, Type.Object({ + x: Type.Union([Type.String(), Type.Number()]) + })) + }) + it('Should guard mapped 11', () => { + const T = Type.Object({ + x: Type.Union([Type.String(), Type.Number(), Type.Boolean()]) + }) + const M = Type.Mapped(Type.KeyOf(T), K => { + return Type.Extract(Type.Index(T, K), Type.Null()) + }) + Assert.IsEqual(M, Type.Object({ + x: Type.Never() + })) + }) + // ---------------------------------------------------------------- + // Extends + // ---------------------------------------------------------------- + it('Should guard mapped 12', () => { + const T = Type.Object({ + x: Type.Number(), + y: Type.String(), + z: Type.Boolean() + }) + const M = Type.Mapped(Type.KeyOf(T), K => { + return ( + Type.Extends(K, Type.Literal('x'), Type.Literal(1), + Type.Extends(K, Type.Literal('y'), Type.Literal(2), + Type.Extends(K, Type.Literal('z'), Type.Literal(3), Type.Never()))) + ) + }) + Assert.IsEqual(M, Type.Object({ + x: Type.Literal(1), + y: Type.Literal(2), + z: Type.Literal(3), + })) + }) + it('Should guard mapped 13', () => { + const T = Type.Object({ + x: Type.Number(), + y: Type.String(), + z: Type.Boolean() + }) + const M = Type.Mapped(Type.KeyOf(T), K => { + return ( + Type.Extends(Type.Index(T, K), Type.Number(), Type.Literal(3), + Type.Extends(Type.Index(T, K), Type.String(), Type.Literal(2), + Type.Extends(Type.Index(T, K), Type.Boolean(), Type.Literal(1), Type.Never()))) + ) + }) + Assert.IsEqual(M, Type.Object({ + x: Type.Literal(3), + y: Type.Literal(2), + z: Type.Literal(1), + })) + }) + // ---------------------------------------------------------------- + // Exclude + // ---------------------------------------------------------------- + it('Should guard mapped 14', () => { + const T = Type.Object({ + x: Type.Union([Type.String(), Type.Number(), Type.Boolean()]) + }) + const M = Type.Mapped(Type.KeyOf(T), K => { + return Type.Exclude(Type.Index(T, K), Type.String()) + }) + Assert.IsEqual(M, Type.Object({ + x: Type.Union([Type.Number(), Type.Boolean()]) + })) + }) + it('Should guard mapped 15', () => { + const T = Type.Object({ + x: Type.Union([Type.String(), Type.Number(), Type.Boolean()]) + }) + const M = Type.Mapped(Type.KeyOf(T), K => { + return Type.Exclude(Type.Index(T, K), Type.Union([ + Type.String(), + Type.Number() + ])) + }) + Assert.IsEqual(M, Type.Object({ + x: Type.Boolean() + })) + }) + it('Should guard mapped 16', () => { + const T = Type.Object({ + x: Type.Union([Type.String(), Type.Number(), Type.Boolean()]) + }) + const M = Type.Mapped(Type.KeyOf(T), K => { + return Type.Exclude(Type.Index(T, K), Type.Null()) + }) + Assert.IsEqual(M, Type.Object({ + x: Type.Union([Type.String(), Type.Number(), Type.Boolean()]) + })) + }) + // ---------------------------------------------------------------- + // Non-Evaluated + // ---------------------------------------------------------------- + it('Should guard mapped 17', () => { + const T = Type.Object({ x: Type.Number() }) + const M = Type.Mapped(Type.KeyOf(T), K => Type.Array(Type.Index(T, K))) + Assert.IsEqual(M, Type.Object({ x: Type.Array(Type.Number()) })) + }) + it('Should guard mapped 18', () => { + const T = Type.Object({ x: Type.Number() }) + const M = Type.Mapped(Type.KeyOf(T), K => Type.Promise(Type.Index(T, K))) + Assert.IsEqual(M, Type.Object({ x: Type.Promise(Type.Number()) })) + }) + it('Should guard mapped 19', () => { + const T = Type.Object({ x: Type.Number() }) + const M = Type.Mapped(Type.KeyOf(T), K => Type.Function([Type.Index(T, K)], Type.Index(T, K))) + Assert.IsEqual(M, Type.Object({ x: Type.Function([Type.Number()], Type.Number())})) + }) + it('Should guard mapped 20', () => { + const T = Type.Object({ x: Type.Number() }) + const M = Type.Mapped(Type.KeyOf(T), K => Type.Tuple([Type.Index(T, K), Type.Index(T, K)])) + Assert.IsEqual(M, Type.Object({ x: Type.Tuple([Type.Number(), Type.Number()]) })) + }) + it('Should guard mapped 21', () => { + const T = Type.Object({ x: Type.Number() }) + const M = Type.Mapped(Type.KeyOf(T), K => Type.Union([Type.Index(T, K), Type.Index(T, K)])) + Assert.IsEqual(M, Type.Object({ x: Type.Union([Type.Number(), Type.Number()]) })) + }) + it('Should guard mapped 22', () => { + const T = Type.Object({ x: Type.Number() }) + const M = Type.Mapped(Type.KeyOf(T), K => Type.Intersect([Type.Index(T, K), Type.Index(T, K)])) + Assert.IsEqual(M, Type.Object({ x: Type.Intersect([Type.Number(), Type.Number()]) })) + }) + // ---------------------------------------------------------------- + // Numeric Keys + // ---------------------------------------------------------------- + it('Should guard mapped 23', () => { + const T = Type.Object({ + 0: Type.Number(), + 1: Type.Number(), + 2: Type.Number() + }) + const M = Type.Mapped(Type.KeyOf(T), K => K) + Assert.IsEqual(M, Type.Object({ + 0: Type.Literal('0'), + 1: Type.Literal('1'), + 2: Type.Literal('2'), + })) + }) + it('Should guard mapped 24', () => { + const T = Type.Object({ + 0: Type.Number(), + 1: Type.Number(), + 2: Type.Number() + }) + const M = Type.Mapped(Type.KeyOf(T), K => Type.Index(T, K)) + Assert.IsEqual(M, Type.Object({ + 0: Type.Number(), + 1: Type.Number(), + 2: Type.Number(), + })) + }) + it('Should guard mapped 25', () => { + const T = Type.Object({ + 0: Type.Number(), + 1: Type.Number(), + 2: Type.Number() + }) + const M = Type.Mapped(Type.KeyOf(T), K => Type.String()) + Assert.IsEqual(M, Type.Object({ + 0: Type.String(), + 1: Type.String(), + 2: Type.String(), + })) + }) + it('Should guard mapped 26', () => { + const T = Type.Object({ + 0: Type.Number(), + 1: Type.Number(), + 2: Type.Number() + }) + const M = Type.Mapped(Type.KeyOf(T), K => Type.Extends(K, Type.Literal('1'), Type.String(), Type.Number())) + Assert.IsEqual(M, Type.Object({ + 0: Type.Number(), + 1: Type.String(), + 2: Type.Number(), + })) + }) + // ---------------------------------------------------------------- + // Modifiers: Optional + // ---------------------------------------------------------------- + it('Should guard mapped 27', () => { + const T = Type.Object({ + x: Type.Optional(Type.Number()), + y: Type.Number() + }) + // subtractive + const M = Type.Mapped(Type.KeyOf(T), K => Type.Optional(Type.Index(T, K), false)) + Assert.IsEqual(M, Type.Object({ + x: Type.Number(), + y: Type.Number() + })) + }) + it('Should guard mapped 28', () => { + const T = Type.Object({ + x: Type.Optional(Type.Number()), + y: Type.Number() + }) + // additive + const M = Type.Mapped(Type.KeyOf(T), K => Type.Optional(Type.Index(T, K), true)) + Assert.IsEqual(M, Type.Object({ + x: Type.Optional(Type.Number()), + y: Type.Optional(Type.Number()) + })) + }) + // ---------------------------------------------------------------- + // Modifiers: Readonly + // ---------------------------------------------------------------- + it('Should guard mapped 27', () => { + const T = Type.Object({ + x: Type.Readonly(Type.Number()), + y: Type.Number() + }) + // subtractive + const M = Type.Mapped(Type.KeyOf(T), K => Type.Readonly(Type.Index(T, K), false)) + Assert.IsEqual(M, Type.Object({ + x: Type.Number(), + y: Type.Number() + })) + }) + it('Should guard mapped 28', () => { + const T = Type.Object({ + x: Type.Readonly(Type.Number()), + y: Type.Number() + }) + // additive + const M = Type.Mapped(Type.KeyOf(T), K => Type.Readonly(Type.Index(T, K), true)) + Assert.IsEqual(M, Type.Object({ + x: Type.Readonly(Type.Number()), + y: Type.Readonly(Type.Number()) + })) + }) + // ---------------------------------------------------------------- + // Finite Boolean + // ---------------------------------------------------------------- + it('Should guard mapped 29', () => { + const T = Type.TemplateLiteral('${boolean}') + const M = Type.Mapped(T, K => K) + Assert.IsEqual(M, Type.Object({ + true: Type.Literal('true'), + false: Type.Literal('false'), + })) + }) + it('Should guard mapped 30', () => { + const T = Type.TemplateLiteral('${0|1}${boolean}') + const M = Type.Mapped(T, K => K) + Assert.IsEqual(M, Type.Object({ + '0true': Type.Literal('0true'), + '0false': Type.Literal('0false'), + '1true': Type.Literal('1true'), + '1false': Type.Literal('1false'), + })) + }) + it('Should guard mapped 31', () => { + const T = Type.TemplateLiteral('${boolean}${0|1}') + const M = Type.Mapped(T, K => K) + Assert.IsEqual(M, Type.Object({ + 'true0': Type.Literal('true0'), + 'true1': Type.Literal('true1'), + 'false0': Type.Literal('false0'), + 'false1': Type.Literal('false1'), + })) + }) + // ---------------------------------------------------------------- + // Numeric Mapping + // ---------------------------------------------------------------- + it('Should guard mapped 32', () => { + const T = Type.TemplateLiteral([ + Type.Union([Type.Literal(0), Type.Literal(1)]), + Type.Union([Type.Literal(0), Type.Literal(1)]), + ]) + const M = Type.Mapped(T, (K) => K) + Assert.IsEqual(M, Type.Object({ + '00': Type.Literal('00'), + '01': Type.Literal('01'), + '10': Type.Literal('10'), + '11': Type.Literal('11'), + })) + }) + // ---------------------------------------------------------------- + // Indexed Key Remap + // ---------------------------------------------------------------- + it('Should guard mapped 33', () => { + const T = Type.Object({ + hello: Type.Number(), + world: Type.String(), + }) + const M = Type.Mapped(Type.Uppercase(Type.KeyOf(T)), (K) => { + return Type.Index(T, Type.Lowercase(K)) + }) + Assert.IsEqual(M, Type.Object({ + HELLO: Type.Number(), + WORLD: Type.String() + })) + }) + // ---------------------------------------------------------------- + // Partial + // ---------------------------------------------------------------- + it('Should guard mapped 34', () => { + const T = Type.Object({ + x: Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + y: Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + }) + const M = Type.Mapped(Type.KeyOf(T), (K) => { + return Type.Partial(Type.Index(T, K)) + }) + Assert.IsEqual(M, Type.Object({ + x: Type.Partial(Type.Object({ + x: Type.Optional(Type.Number()), + y: Type.Optional(Type.Number()), + })), + y: Type.Partial(Type.Object({ + x: Type.Optional(Type.Number()), + y: Type.Optional(Type.Number()), + })), + })) + }) + // ---------------------------------------------------------------- + // Required + // ---------------------------------------------------------------- + it('Should guard mapped 35', () => { + const T = Type.Object({ + x: Type.Partial(Type.Object({ + x: Type.Number(), + y: Type.Number(), + })), + y: Type.Partial(Type.Object({ + x: Type.Number(), + y: Type.Number(), + })), + }) + const M = Type.Mapped(Type.KeyOf(T), (K) => { + return Type.Required(Type.Index(T, K)) + }) + Assert.IsEqual(M, Type.Object({ + x: Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + y: Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + })) + }) + // ------------------------------------------------------------------ + // Pick With Key + // ------------------------------------------------------------------ + it('Should guard mapped 36', () => { + const T = Type.Object({ + x: Type.Object({ + x: Type.Number(), + y: Type.Number() + }), + y: Type.Object({ + x: Type.Number(), + y: Type.Number() + }) + }) + const M = Type.Mapped(Type.KeyOf(T), K => { + return Type.Pick(T, K) + }) + Assert.IsEqual(M, Type.Object({ + x: Type.Object({ + x: Type.Object({ + x: Type.Number(), + y: Type.Number(), + }) + }), + y: Type.Object({ + y: Type.Object({ + x: Type.Number(), + y: Type.Number(), + }) + }), + })) + }) + // ------------------------------------------------------------------ + // Pick With Result + // ------------------------------------------------------------------ + it('Should guard mapped 37', () => { + const T = Type.Object({ + x: Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + y: Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + }) + const M = Type.Mapped(Type.KeyOf(T), (K) => { + return Type.Pick(Type.Index(T, K), ['x']) + }) + Assert.IsEqual(M, Type.Object({ + x: Type.Object({ x: Type.Number() }), + y: Type.Object({ x: Type.Number() }) + })) + }) + // ------------------------------------------------------------------ + // Omit With Key + // ------------------------------------------------------------------ + it('Should guard mapped 36', () => { + const T = Type.Object({ + x: Type.Object({ + x: Type.Number(), + y: Type.Number() + }), + y: Type.Object({ + x: Type.Number(), + y: Type.Number() + }) + }) + const M = Type.Mapped(Type.KeyOf(T), K => { + return Type.Omit(T, K) + }) + Assert.IsEqual(M, Type.Object({ + x: Type.Object({ + y: Type.Object({ + x: Type.Number(), + y: Type.Number(), + }) + }), + y: Type.Object({ + x: Type.Object({ + x: Type.Number(), + y: Type.Number(), + }) + }), + })) + }) + // ------------------------------------------------------------------ + // Omit With Result + // ------------------------------------------------------------------ + it('Should guard mapped 37', () => { + const T = Type.Object({ + x: Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + y: Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + }) + const M = Type.Mapped(Type.KeyOf(T), (K) => { + return Type.Omit(Type.Index(T, K), ['x']) + }) + Assert.IsEqual(M, Type.Object({ + x: Type.Object({ y: Type.Number() }), + y: Type.Object({ y: Type.Number() }) + })) + }) +}) diff --git a/test/runtime/type/guard/type/not.ts b/test/runtime/type/guard/type/not.ts new file mode 100644 index 000000000..fcfd8b787 --- /dev/null +++ b/test/runtime/type/guard/type/not.ts @@ -0,0 +1,19 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TNot', () => { + it('Should guard for TNot', () => { + const R = TypeGuard.IsNot(Type.Not(Type.String())) + Assert.IsTrue(R) + }) + it('Should not guard for TNot 1', () => { + const R = TypeGuard.IsNot(Type.Not(null as any)) + Assert.IsFalse(R) + }) + it('Should not guard for TNot with invalid $id', () => { + // @ts-ignore + const R = TypeGuard.IsNot(Type.Not(Type.String()), { $id: 1 }) + Assert.IsTrue(R) + }) +}) diff --git a/test/runtime/type/guard/type/null.ts b/test/runtime/type/guard/type/null.ts new file mode 100644 index 000000000..fc2e85fd5 --- /dev/null +++ b/test/runtime/type/guard/type/null.ts @@ -0,0 +1,19 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TNull', () => { + it('Should guard for TNull', () => { + const R = TypeGuard.IsNull(Type.Null()) + Assert.IsTrue(R) + }) + it('Should not guard for TNull', () => { + const R = TypeGuard.IsNull(null) + Assert.IsFalse(R) + }) + it('Should not guard for TNull with invalid $id', () => { + // @ts-ignore + const R = TypeGuard.IsNull(Type.Null({ $id: 1 })) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/guard/type/number.ts b/test/runtime/type/guard/type/number.ts new file mode 100644 index 000000000..c4550aeba --- /dev/null +++ b/test/runtime/type/guard/type/number.ts @@ -0,0 +1,44 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TNumber', () => { + it('Should guard for TNumber', () => { + const R = TypeGuard.IsNumber(Type.Number()) + Assert.IsTrue(R) + }) + it('Should not guard for TNumber', () => { + const R = TypeGuard.IsNumber(null) + Assert.IsFalse(R) + }) + it('Should not guard for TNumber with invalid $id', () => { + // @ts-ignore + const R = TypeGuard.IsNumber(Type.Number({ $id: 1 })) + Assert.IsFalse(R) + }) + it('Should not guard for TNumber with invalid multipleOf', () => { + // @ts-ignore + const R = TypeGuard.IsNumber(Type.Number({ multipleOf: '1' })) + Assert.IsFalse(R) + }) + it('Should not guard for TNumber with invalid minimum', () => { + // @ts-ignore + const R = TypeGuard.IsNumber(Type.Number({ minimum: '1' })) + Assert.IsFalse(R) + }) + it('Should not guard for TNumber with invalid maximum', () => { + // @ts-ignore + const R = TypeGuard.IsNumber(Type.Number({ maximum: '1' })) + Assert.IsFalse(R) + }) + it('Should not guard for TNumber with invalid exclusiveMinimum', () => { + // @ts-ignore + const R = TypeGuard.IsNumber(Type.Number({ exclusiveMinimum: '1' })) + Assert.IsFalse(R) + }) + it('Should not guard for TNumber with invalid exclusiveMaximum', () => { + // @ts-ignore + const R = TypeGuard.IsNumber(Type.Number({ exclusiveMaximum: '1' })) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/guard/type/object.ts b/test/runtime/type/guard/type/object.ts new file mode 100644 index 000000000..49368dd36 --- /dev/null +++ b/test/runtime/type/guard/type/object.ts @@ -0,0 +1,121 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TObject', () => { + it('Should guard for TObject', () => { + const R = TypeGuard.IsObject( + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + ) + Assert.IsTrue(R) + }) + it('Should not guard for TObject', () => { + const R = TypeGuard.IsObject(null) + Assert.IsFalse(R) + }) + it('Should not guard for TObject with escape characters in property key', () => { + const R = TypeGuard.IsObject( + Type.Object({ + 'hello\nworld': Type.Number(), + }), + ) + Assert.IsFalse(R) + }) + it('Should not guard for TObject with invalid property values', () => { + const R = TypeGuard.IsObject( + Type.Object({ + x: Type.Number(), + y: {} as any, + }), + ) + Assert.IsFalse(R) + }) + it('Should not guard for TObject with invalid additionalProperties', () => { + const R = TypeGuard.IsObject( + Type.Object( + { + x: Type.Number(), + }, + { + // @ts-ignore + additionalProperties: 'true', + }, + ), + ) + Assert.IsFalse(R) + }) + it('Should not guard for TObject with invalid $id', () => { + const R = TypeGuard.IsObject( + Type.Object( + { + x: Type.Number(), + }, + { + // @ts-ignore + $id: 1, + }, + ), + ) + Assert.IsFalse(R) + }) + it('Should not guard for TObject with invalid minProperties', () => { + const R = TypeGuard.IsObject( + Type.Object( + { + x: Type.Number(), + }, + { + // @ts-ignore + minProperties: '1', + }, + ), + ) + Assert.IsFalse(R) + }) + it('Should not guard for TObject with invalid maxProperties', () => { + const R = TypeGuard.IsObject( + Type.Object( + { + x: Type.Number(), + }, + { + // @ts-ignore + maxProperties: '1', + }, + ), + ) + Assert.IsFalse(R) + }) + it('Should guard for TObject with invalid additional properties', () => { + const R = TypeGuard.IsObject( + Type.Object( + { + x: Type.Number(), + y: Type.Number(), + }, + { + // @ts-ignore + additionalProperties: 1, + }, + ), + ) + Assert.IsFalse(R) + }) + it('Should not guard for TObject with valid additional properties schema', () => { + const R = TypeGuard.IsObject( + Type.Object( + { + x: Type.Number(), + y: Type.Number(), + }, + { + additionalProperties: Type.String(), + }, + ), + ) + Assert.IsTrue(R) + }) +}) diff --git a/test/runtime/type/guard/type/omit.ts b/test/runtime/type/guard/type/omit.ts new file mode 100644 index 000000000..cbfa16109 --- /dev/null +++ b/test/runtime/type/guard/type/omit.ts @@ -0,0 +1,169 @@ +import { TypeGuard, Type, Kind, TransformKind } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TOmit', () => { + // ------------------------------------------------------------------------- + // case: https://github.com/sinclairzx81/typebox/issues/384 + // ------------------------------------------------------------------------- + it('Should support TUnsafe omit properties with no Kind', () => { + const T = Type.Omit(Type.Object({ x: Type.Unsafe({ x: 1 }), y: Type.Number() }), ['x']) + Assert.IsEqual(T.required, ['y']) + }) + it('Should support TUnsafe omit properties with unregistered Kind', () => { + const T = Type.Omit(Type.Object({ x: Type.Unsafe({ x: 1, [Kind]: 'UnknownOmitType' }), y: Type.Number() }), ['x']) + Assert.IsEqual(T.required, ['y']) + }) + // ------------------------------------------------------------------------- + // Standard Tests + // ------------------------------------------------------------------------- + it('Should Omit 1', () => { + const T = Type.Omit( + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + ['x'], + ) + Assert.IsTrue(TypeGuard.IsNumber(T.properties.y)) + Assert.IsEqual(T.required, ['y']) + }) + it('Should Omit 2', () => { + const T = Type.Omit( + Type.Object({ + x: Type.Number(), + y: Type.Optional(Type.Number()), + }), + ['x'], + ) + Assert.IsTrue(TypeGuard.IsNumber(T.properties.y)) + Assert.IsEqual(T.required, undefined) + }) + it('Should Omit 3', () => { + const L = Type.Literal('x') + const T = Type.Omit( + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + L, + ) + Assert.IsTrue(TypeGuard.IsNumber(T.properties.y)) + Assert.IsEqual(T.required, ['y']) + }) + it('Should Omit 4', () => { + const L = Type.Literal('x') + const T = Type.Omit(Type.Intersect([Type.Object({ x: Type.Number() }), Type.Object({ y: Type.Number() })]), L) + Assert.IsEqual(TypeGuard.IsNumber(T.allOf[1].properties.y), true) + // @ts-ignore + Assert.IsEqual(T.allOf[1].properties.x, undefined) + }) + it('Should Omit 5', () => { + const L = Type.Union([Type.Literal('x'), Type.Literal('y')]) + const T = Type.Omit( + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + L, + ) + // @ts-ignore + Assert.IsEqual(T.properties.x, undefined) + // @ts-ignore + Assert.IsEqual(T.properties.y, undefined) + // @ts-ignore + Assert.IsEqual(T.required, undefined) + }) + it('Should Omit 6', () => { + const L = Type.Union([Type.Literal('x'), Type.Literal('y'), Type.Literal('z')]) + const T = Type.Omit( + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + L, + ) + // @ts-ignore + Assert.IsEqual(T.properties.x, undefined) + // @ts-ignore + Assert.IsEqual(T.properties.y, undefined) + // @ts-ignore + Assert.IsEqual(T.required, undefined) + }) + it('Should Omit 7', () => { + const L = Type.TemplateLiteral([Type.Literal('a'), Type.Union([Type.Literal('b'), Type.Literal('c')])]) + const T = Type.Omit( + Type.Object({ + ab: Type.Number(), + ac: Type.Number(), + ad: Type.Number(), + }), + L, + ) + Assert.IsTrue(TypeGuard.IsNumber(T.properties.ad)) + Assert.IsEqual(T.required, ['ad']) + }) + // ---------------------------------------------------------------- + // Discard + // ---------------------------------------------------------------- + it('Should override $id', () => { + const A = Type.Object({ x: Type.Number() }, { $id: 'A' }) + const T = Type.Omit(A, ['x'], { $id: 'T' }) + Assert.IsEqual(T.$id!, 'T') + }) + it('Should discard $id', () => { + const A = Type.Object({ x: Type.Number() }, { $id: 'A' }) + const T = Type.Omit(A, ['x']) + Assert.IsFalse('$id' in T) + }) + it('Should discard transform', () => { + const T = Type.Object({ + x: Type.Number(), + y: Type.String(), + }) + const S = Type.Transform(T) + .Decode((value) => value) + .Encode((value) => value) + const R = Type.Omit(S, ['x']) + Assert.IsFalse(TransformKind in R) + }) + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/944 + // ---------------------------------------------------------------- + it('Should retain interior properties 1', () => { + const A = Type.Object({ x: Type.Number() }, { additionalProperties: false }) + const T = Type.Omit(A, ['x']) + Assert.IsFalse(T.additionalProperties as boolean) + }) + it('Should retain interior properties 2', () => { + const A = Type.Object({ x: Type.Number() }, { additionalProperties: false }) + const B = Type.Object({ y: Type.Number() }, { additionalProperties: false }) + const U = Type.Union([A, B]) + const T = Type.Omit(U, ['x']) + Assert.IsFalse(T.anyOf[0].additionalProperties as boolean) + Assert.IsFalse(T.anyOf[1].additionalProperties as boolean) + }) + it('Should retain interior properties 3', () => { + const A = Type.Object({ x: Type.Number() }, { additionalProperties: false }) + const B = Type.Object({ y: Type.Number() }, { additionalProperties: false }) + const U = Type.Intersect([A, B]) + const T = Type.Omit(U, ['x']) + Assert.IsFalse(T.allOf[0].additionalProperties as boolean) + Assert.IsFalse(T.allOf[1].additionalProperties as boolean) + }) + it('Should retain interior properties 4', () => { + const A = Type.Object({ x: Type.Number(), y: Type.Number() }, { additionalProperties: false }) + const T = Type.Mapped(Type.TemplateLiteral('${x|y|z}'), (_) => Type.Omit(A, ['x'])) + Assert.IsFalse(T.properties.x.additionalProperties as boolean) + Assert.IsFalse(T.properties.y.additionalProperties as boolean) + Assert.IsFalse(T.properties.z.additionalProperties as boolean) + }) + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/980 + // ---------------------------------------------------------------- + it('Should override properties in source type', () => { + const A = Type.Object({ x: Type.Number() }, { title: 'A' }) + const B = Type.Omit(A, ['x'], { title: 'B' }) + Assert.IsEqual(A.title, 'A') + Assert.IsEqual(B.title, 'B') + }) +}) diff --git a/test/runtime/type/guard/type/partial.ts b/test/runtime/type/guard/type/partial.ts new file mode 100644 index 000000000..c2cb21312 --- /dev/null +++ b/test/runtime/type/guard/type/partial.ts @@ -0,0 +1,108 @@ +import { TypeGuard, TypeRegistry, Type, Kind, TransformKind } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TPartial', () => { + it('Should produce a valid TSchema', () => { + const T = Type.Partial(Type.Object({ x: Type.Number() })) + Assert.IsTrue(TypeGuard.IsSchema(T)) + }) + // ------------------------------------------------------------------------- + // case: https://github.com/sinclairzx81/typebox/issues/364 + // ------------------------------------------------------------------------- + it('Should support TUnsafe partial properties with no Kind', () => { + const T = Type.Partial(Type.Object({ x: Type.Unsafe({ x: 1 }) })) + Assert.IsEqual(T.required, undefined) + }) + it('Should support TUnsafe partial properties with unknown Kind', () => { + const T = Type.Partial(Type.Object({ x: Type.Unsafe({ [Kind]: 'UnknownPartialType', x: 1 }) })) + Assert.IsEqual(T.required, undefined) + }) + it('Should support TUnsafe partial properties with known Kind', () => { + TypeRegistry.Set('KnownPartialType', () => true) + const T = Type.Partial(Type.Object({ x: Type.Unsafe({ [Kind]: 'KnownPartialType', x: 1 }) })) + Assert.IsEqual(T.required, undefined) + }) + it('Should support applying partial to intersect', () => { + const A = Type.Object({ x: Type.Number() }) + const B = Type.Object({ y: Type.Number() }) + const I = Type.Intersect([A, B]) + const T = Type.Partial(I) + Assert.IsEqual(T.allOf.length, 2) + Assert.IsEqual(T.allOf[0].required, undefined) + Assert.IsEqual(T.allOf[1].required, undefined) + }) + it('Should support applying partial to union', () => { + const A = Type.Object({ x: Type.Number() }) + const B = Type.Object({ y: Type.Number() }) + const I = Type.Union([A, B]) + const T = Type.Partial(I) + Assert.IsEqual(T.anyOf.length, 2) + Assert.IsEqual(T.anyOf[0].required, undefined) + Assert.IsEqual(T.anyOf[1].required, undefined) + }) + // ---------------------------------------------------------------- + // Discard + // ---------------------------------------------------------------- + it('Should override $id', () => { + const A = Type.Object({ x: Type.Number() }, { $id: 'A' }) + const T = Type.Partial(A, { $id: 'T' }) + Assert.IsEqual(T.$id!, 'T') + }) + it('Should discard $id', () => { + const A = Type.Object({ x: Type.Number() }, { $id: 'A' }) + const T = Type.Partial(A) + Assert.IsFalse('$id' in T) + }) + it('Should discard transform', () => { + const T = Type.Object({ + x: Type.Number(), + y: Type.String(), + }) + const S = Type.Transform(T) + .Decode((value) => value) + .Encode((value) => value) + const R = Type.Partial(S) + Assert.IsFalse(TransformKind in R) + }) + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/980 + // ---------------------------------------------------------------- + it('Should override properties in source type', () => { + const A = Type.Object({ x: Type.Number() }, { title: 'A' }) + const B = Type.Partial(A, { title: 'B' }) + Assert.IsEqual(A.title, 'A') + Assert.IsEqual(B.title, 'B') + }) + // ------------------------------------------------------------------ + // Intrinsic Passthough + // https://github.com/sinclairzx81/typebox/issues/1169 + // ------------------------------------------------------------------ + it('Should pass through on intrinsic types on union 1', () => { + const T = Type.Partial( + Type.Union([ + Type.Number(), + Type.Object({ + x: Type.Number(), + }), + ]), + ) + Assert.IsTrue(TypeGuard.IsUnion(T)) + Assert.IsTrue(TypeGuard.IsNumber(T.anyOf[0])) + Assert.IsTrue(TypeGuard.IsObject(T.anyOf[1])) + Assert.IsTrue(TypeGuard.IsOptional(T.anyOf[1].properties.x)) + }) + it('Should pass through on intrinsic types on union 2', () => { + const T = Type.Partial( + Type.Union([ + Type.Literal(1), + Type.Object({ + x: Type.Number(), + }), + ]), + ) + Assert.IsTrue(TypeGuard.IsUnion(T)) + Assert.IsTrue(TypeGuard.IsLiteral(T.anyOf[0])) + Assert.IsTrue(TypeGuard.IsObject(T.anyOf[1])) + Assert.IsTrue(TypeGuard.IsOptional(T.anyOf[1].properties.x)) + }) +}) diff --git a/test/runtime/type/guard/type/pick.ts b/test/runtime/type/guard/type/pick.ts new file mode 100644 index 000000000..65153aab7 --- /dev/null +++ b/test/runtime/type/guard/type/pick.ts @@ -0,0 +1,171 @@ +import { TypeGuard, Type, Kind, TransformKind } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TPick', () => { + // ------------------------------------------------------------------------- + // case: https://github.com/sinclairzx81/typebox/issues/384 + // ------------------------------------------------------------------------- + it('Should support TUnsafe omit properties with no Kind', () => { + const T = Type.Pick( + Type.Object({ + x: Type.Unsafe({ x: 1 }), + y: Type.Number(), + }), + ['x'], + ) + Assert.IsEqual(T.required, ['x']) + }) + it('Should support TUnsafe omit properties with unregistered Kind', () => { + const T = Type.Pick(Type.Object({ x: Type.Unsafe({ x: 1, [Kind]: 'UnknownPickType' }), y: Type.Number() }), ['x']) + Assert.IsEqual(T.required, ['x']) + }) + // ------------------------------------------------------------------------- + // Standard Tests + // ------------------------------------------------------------------------- + it('Should Pick 1', () => { + const T = Type.Pick( + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + ['x'], + ) + Assert.IsTrue(TypeGuard.IsNumber(T.properties.x)) + Assert.IsEqual(T.required, ['x']) + }) + it('Should Pick 2', () => { + const T = Type.Pick( + Type.Object({ + x: Type.Optional(Type.Number()), + y: Type.Number(), + }), + ['x'], + ) + Assert.IsTrue(TypeGuard.IsNumber(T.properties.x)) + Assert.IsEqual(T.required, undefined) + }) + it('Should Pick 3', () => { + const L = Type.Literal('x') + const T = Type.Pick( + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + L, + ) + Assert.IsTrue(TypeGuard.IsNumber(T.properties.x)) + Assert.IsEqual(T.required, ['x']) + }) + it('Should Pick 4', () => { + const L = Type.Literal('x') + const T = Type.Pick(Type.Intersect([Type.Object({ x: Type.Number() }), Type.Object({ y: Type.Number() })]), L) + + Assert.IsTrue(TypeGuard.IsNumber(T.allOf[0].properties.x)) + // @ts-ignore + Assert.IsEqual(T.allOf[1].properties.y, undefined) + }) + it('Should Pick 5', () => { + const L = Type.Union([Type.Literal('x'), Type.Literal('y')]) + const T = Type.Pick( + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + L, + ) + Assert.IsTrue(TypeGuard.IsNumber(T.properties.x)) + Assert.IsTrue(TypeGuard.IsNumber(T.properties.y)) + Assert.IsEqual(T.required, ['x', 'y']) + }) + it('Should Pick 6', () => { + const L = Type.Union([Type.Literal('x'), Type.Literal('y'), Type.Literal('z')]) + const T = Type.Pick( + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + L, + ) + Assert.IsTrue(TypeGuard.IsNumber(T.properties.x)) + Assert.IsTrue(TypeGuard.IsNumber(T.properties.y)) + Assert.IsEqual(T.required, ['x', 'y']) + }) + it('Should Pick 7', () => { + const L = Type.TemplateLiteral([Type.Literal('a'), Type.Union([Type.Literal('b'), Type.Literal('c')])]) + const T = Type.Pick( + Type.Object({ + ab: Type.Number(), + ac: Type.Number(), + ad: Type.Number(), + }), + L, + ) + Assert.IsTrue(TypeGuard.IsNumber(T.properties.ab)) + Assert.IsTrue(TypeGuard.IsNumber(T.properties.ac)) + Assert.IsEqual(T.required, ['ab', 'ac']) + }) + // ---------------------------------------------------------------- + // Discard + // ---------------------------------------------------------------- + it('Should override $id', () => { + const A = Type.Object({ x: Type.Number() }, { $id: 'A' }) + const T = Type.Pick(A, ['x'], { $id: 'T' }) + Assert.IsEqual(T.$id!, 'T') + }) + it('Should discard $id', () => { + const A = Type.Object({ x: Type.Number() }, { $id: 'A' }) + const T = Type.Pick(A, ['x']) + Assert.IsFalse('$id' in T) + }) + it('Should discard transform', () => { + const T = Type.Object({ + x: Type.Number(), + y: Type.String(), + }) + const S = Type.Transform(T) + .Decode((value) => value) + .Encode((value) => value) + const R = Type.Pick(S, ['x']) + Assert.IsFalse(TransformKind in R) + }) + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/944 + // ---------------------------------------------------------------- + it('Should retain interior properties 1', () => { + const A = Type.Object({ x: Type.Number() }, { additionalProperties: false }) + const T = Type.Pick(A, ['x']) + Assert.IsFalse(T.additionalProperties as boolean) + }) + it('Should retain interior properties 2', () => { + const A = Type.Object({ x: Type.Number() }, { additionalProperties: false }) + const B = Type.Object({ y: Type.Number() }, { additionalProperties: false }) + const U = Type.Union([A, B]) + const T = Type.Pick(U, ['x']) + Assert.IsFalse(T.anyOf[0].additionalProperties as boolean) + Assert.IsFalse(T.anyOf[1].additionalProperties as boolean) + }) + it('Should retain interior properties 3', () => { + const A = Type.Object({ x: Type.Number() }, { additionalProperties: false }) + const B = Type.Object({ y: Type.Number() }, { additionalProperties: false }) + const U = Type.Intersect([A, B]) + const T = Type.Pick(U, ['x']) + Assert.IsFalse(T.allOf[0].additionalProperties as boolean) + Assert.IsFalse(T.allOf[1].additionalProperties as boolean) + }) + it('Should retain interior properties 4', () => { + const A = Type.Object({ x: Type.Number(), y: Type.Number() }, { additionalProperties: false }) + const T = Type.Mapped(Type.TemplateLiteral('${x|y|z}'), (_) => Type.Pick(A, ['x'])) + Assert.IsFalse(T.properties.x.additionalProperties as boolean) + Assert.IsFalse(T.properties.y.additionalProperties as boolean) + Assert.IsFalse(T.properties.z.additionalProperties as boolean) + }) + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/980 + // ---------------------------------------------------------------- + it('Should override properties in source type', () => { + const A = Type.Object({ x: Type.Number() }, { title: 'A' }) + const B = Type.Pick(A, ['x'], { title: 'B' }) + Assert.IsEqual(A.title, 'A') + Assert.IsEqual(B.title, 'B') + }) +}) diff --git a/test/runtime/type/guard/type/promise.ts b/test/runtime/type/guard/type/promise.ts new file mode 100644 index 000000000..073dbcf70 --- /dev/null +++ b/test/runtime/type/guard/type/promise.ts @@ -0,0 +1,41 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TPromise', () => { + it('Should guard for TPromise', () => { + const R = TypeGuard.IsPromise(Type.Promise(Type.Number())) + Assert.IsTrue(R) + }) + it('Should not guard for TPromise', () => { + const R = TypeGuard.IsPromise(null) + Assert.IsFalse(R) + }) + it('Should not guard for TPromise with invalid $id', () => { + // @ts-ignore + const R = TypeGuard.IsPromise(Type.Promise(Type.Number(), { $id: 1 })) + Assert.IsFalse(R) + }) + it('Should guard for TPromise with nested TObject', () => { + const R = TypeGuard.IsPromise( + Type.Promise( + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + ), + ) + Assert.IsTrue(R) + }) + it('Should not guard for TPromise with nested TObject', () => { + const R = TypeGuard.IsPromise( + Type.Promise( + Type.Object({ + x: Type.Number(), + y: {} as any, + }), + ), + ) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/guard/type/record.ts b/test/runtime/type/guard/type/record.ts new file mode 100644 index 000000000..0bf69119a --- /dev/null +++ b/test/runtime/type/guard/type/record.ts @@ -0,0 +1,214 @@ +import { TypeGuard, PatternNumberExact, PatternStringExact, PatternNeverExact, PatternString, PatternNumber } from '@sinclair/typebox' +import { Type, RecordKey, RecordValue, RecordPattern } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TRecord', () => { + // ------------------------------------------------------------- + // Overloads + // ------------------------------------------------------------- + it('Should guard overload 1', () => { + const T = Type.Record(Type.Union([Type.Literal('A'), Type.Literal('B')]), Type.String(), { extra: 1 }) + Assert.IsTrue(TypeGuard.IsObject(T)) + Assert.IsTrue(TypeGuard.IsString(T.properties.A)) + Assert.IsTrue(TypeGuard.IsString(T.properties.B)) + Assert.IsEqual(T.extra, 1) + }) + it('Should guard overload 2', () => { + const T = Type.Record(Type.Union([Type.Literal('A')]), Type.String(), { extra: 1 }) // unwrap as literal + Assert.IsTrue(TypeGuard.IsObject(T)) + Assert.IsTrue(TypeGuard.IsString(T.properties.A)) + Assert.IsEqual(T.extra, 1) + }) + it('Should guard overload 3', () => { + // @ts-ignore + const N = Type.Union([]) // Never + const T = Type.Record(N, Type.String(), { extra: 1 }) + Assert.IsTrue(TypeGuard.IsRecord(T)) + Assert.IsTrue(TypeGuard.IsString(T.patternProperties[PatternNeverExact])) + Assert.IsEqual(T.extra, 1) + }) + it('Should guard overload 4', () => { + // @ts-ignore + const T = Type.Record(Type.BigInt(), Type.String(), { extra: 1 }) + Assert.IsTrue(TypeGuard.IsNever(T)) + }) + it('Should guard overload 5', () => { + const T = Type.Record(Type.Literal('A'), Type.String(), { extra: 1 }) + Assert.IsTrue(TypeGuard.IsObject(T)) + Assert.IsTrue(TypeGuard.IsString(T.properties.A)) + Assert.IsEqual(T.extra, 1) + }) + it('Should guard overload 6', () => { + const L = Type.TemplateLiteral([Type.Literal('hello'), Type.Union([Type.Literal('A'), Type.Literal('B')])]) + const T = Type.Record(L, Type.String(), { extra: 1 }) + Assert.IsTrue(TypeGuard.IsObject(T)) + Assert.IsTrue(TypeGuard.IsString(T.properties.helloA)) + Assert.IsTrue(TypeGuard.IsString(T.properties.helloB)) + Assert.IsEqual(T.extra, 1) + }) + it('Should guard overload 7', () => { + const T = Type.Record(Type.Number(), Type.String(), { extra: 1 }) + Assert.IsTrue(TypeGuard.IsRecord(T)) + Assert.IsTrue(TypeGuard.IsString(T.patternProperties[PatternNumberExact])) + Assert.IsEqual(T.extra, 1) + }) + it('Should guard overload 8', () => { + const T = Type.Record(Type.Integer(), Type.String(), { extra: 1 }) + Assert.IsTrue(TypeGuard.IsRecord(T)) + Assert.IsTrue(TypeGuard.IsString(T.patternProperties[PatternNumberExact])) + Assert.IsEqual(T.extra, 1) + }) + it('Should guard overload 9', () => { + const T = Type.Record(Type.String(), Type.String(), { extra: 1 }) + Assert.IsTrue(TypeGuard.IsRecord(T)) + Assert.IsTrue(TypeGuard.IsString(T.patternProperties[PatternStringExact])) + Assert.IsEqual(T.extra, 1) + }) + it('Should guard overload 10', () => { + const L = Type.TemplateLiteral([Type.String(), Type.Literal('_foo')]) + const T = Type.Record(L, Type.String(), { extra: 1 }) + Assert.IsTrue(TypeGuard.IsRecord(T)) + Assert.IsTrue(TypeGuard.IsString(T.patternProperties[`^${PatternString}_foo$`])) + Assert.IsEqual(T.extra, 1) + }) + it('Should guard overload 11', () => { + const L = Type.Union([Type.Literal('A'), Type.Union([Type.Literal('B'), Type.Literal('C')])]) + const T = Type.Record(L, Type.String()) + Assert.IsTrue(TypeGuard.IsObject(T)) + Assert.IsTrue(TypeGuard.IsString(T.properties.A)) + Assert.IsTrue(TypeGuard.IsString(T.properties.B)) + Assert.IsTrue(TypeGuard.IsString(T.properties.C)) + }) + it('Should guard overload 12', () => { + enum E { + A = 'X', + B = 'Y', + C = 'Z', + } + const T = Type.Enum(E) + const R = Type.Record(T, Type.Null()) + Assert.IsTrue(TypeGuard.IsObject(R)) + Assert.IsTrue(TypeGuard.IsNull(R.properties.X)) + Assert.IsTrue(TypeGuard.IsNull(R.properties.Y)) + Assert.IsTrue(TypeGuard.IsNull(R.properties.Z)) + }) + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/916 + // + // Added overload for Any and Never Keys + // ---------------------------------------------------------------- + it('Should guard overload 13', () => { + // @ts-ignore + const T = Type.Record(Type.Never(), Type.String(), { extra: 1 }) + Assert.IsTrue(TypeGuard.IsRecord(T)) + Assert.IsTrue(TypeGuard.IsString(T.patternProperties[PatternNeverExact])) + Assert.IsEqual(T.extra, 1) + }) + it('Should guard overload 14', () => { + // @ts-ignore + const T = Type.Record(Type.Any(), Type.String(), { extra: 1 }) + Assert.IsTrue(TypeGuard.IsRecord(T)) + Assert.IsTrue(TypeGuard.IsString(T.patternProperties[PatternStringExact])) + Assert.IsEqual(T.extra, 1) + }) + // ------------------------------------------------------------- + // Variants + // ------------------------------------------------------------- + it('Should guard for TRecord', () => { + const R = TypeGuard.IsRecord(Type.Record(Type.String(), Type.Number())) + Assert.IsTrue(R) + }) + it('Should guard for TRecord with TObject value', () => { + const R = TypeGuard.IsRecord( + Type.Record( + Type.String(), + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + ), + ) + Assert.IsTrue(R) + }) + it('Should not guard for TRecord', () => { + const R = TypeGuard.IsRecord(null) + Assert.IsFalse(R) + }) + it('Should not guard for TRecord with invalid $id', () => { + // @ts-ignore + const R = TypeGuard.IsRecord(Type.Record(Type.String(), Type.Number(), { $id: 1 })) + Assert.IsFalse(R) + }) + it('Should not guard for TRecord with TObject value with invalid Property', () => { + const R = TypeGuard.IsRecord( + Type.Record( + Type.String(), + Type.Object({ + x: Type.Number(), + y: {} as any, + }), + ), + ) + Assert.IsFalse(R) + }) + it('Normalize: Should normalize to TObject for single literal union value', () => { + const K = Type.Union([Type.Literal('ok')]) + const R = TypeGuard.IsObject(Type.Record(K, Type.Number())) + Assert.IsTrue(R) + }) + it('Normalize: Should normalize to TObject for multi literal union value', () => { + const K = Type.Union([Type.Literal('A'), Type.Literal('B')]) + const R = TypeGuard.IsObject(Type.Record(K, Type.Number())) + Assert.IsTrue(R) + }) + it('Normalize: Should normalize boolean key into true and false', () => { + const K = Type.Boolean() + const R = Type.Record(K, Type.Number()) + Assert.IsTrue(TypeGuard.IsObject(R)) + Assert.IsTrue(TypeGuard.IsNumber(R.properties.true)) + Assert.IsTrue(TypeGuard.IsNumber(R.properties.false)) + }) + // ------------------------------------------------------------------ + // Utility Types + // ------------------------------------------------------------------ + it('Should return RecordPattern', () => { + const R = Type.Record(Type.Number(), Type.Number()) + const K = RecordPattern(R) + Assert.IsTrue(typeof K === 'string') + }) + it('Should return RecordKey (Number)', () => { + const R = Type.Record(Type.Number(), Type.Number()) + const K = RecordKey(R) + Assert.IsTrue(TypeGuard.IsNumber(K)) + }) + it('Should return RecordKey (String)', () => { + const R = Type.Record(Type.String(), Type.Number()) + const K = RecordKey(R) + Assert.IsTrue(TypeGuard.IsString(K)) + }) + it('Should return RecordKey (RegExp)', () => { + const R = Type.Record(Type.RegExp(/(a|b)/), Type.Number()) + const K = RecordKey(R) + Assert.IsTrue(TypeGuard.IsString(K)) // facade + }) + it('Should return RecordValue', () => { + const R = Type.Record(Type.String(), Type.Literal(12345)) + const V = RecordValue(R) + Assert.IsTrue(TypeGuard.IsLiteral(V)) + Assert.IsEqual(V.const, 12345) + }) + // ------------------------------------------------------------------ + // Evaluated: Dollar Sign Escape + // https://github.com/sinclairzx81/typebox/issues/794 + // ------------------------------------------------------------------ + // prettier-ignore + { + const K = Type.TemplateLiteral('$prop${A|B|C}') // issue + const T = Type.Record(K, Type.String()) + Assert.IsTrue(TypeGuard.IsObject(T)) + Assert.IsTrue(TypeGuard.IsString(T.properties.$propA)) + Assert.IsTrue(TypeGuard.IsString(T.properties.$propB)) + Assert.IsTrue(TypeGuard.IsString(T.properties.$propC)) + Assert.IsEqual(T.required, ['$propA', '$propB', '$propC']) + } +}) diff --git a/test/runtime/type/guard/type/recursive.ts b/test/runtime/type/guard/type/recursive.ts new file mode 100644 index 000000000..861b06668 --- /dev/null +++ b/test/runtime/type/guard/type/recursive.ts @@ -0,0 +1,16 @@ +import { TypeGuard, PatternNumberExact, PatternStringExact, PatternString, PatternNumber } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TRecursive', () => { + it('Should guard 1', () => { + const T = Type.Recursive((This) => Type.Object({ nodes: This })) + Assert.IsTrue(TypeGuard.IsRecursive(T)) + Assert.IsTrue(TypeGuard.IsObject(T)) + }) + it('Should guard 2', () => { + const T = Type.Recursive((This) => Type.Tuple([This])) + Assert.IsTrue(TypeGuard.IsRecursive(T)) + Assert.IsTrue(TypeGuard.IsTuple(T)) + }) +}) diff --git a/test/runtime/type/guard/type/ref.ts b/test/runtime/type/guard/type/ref.ts new file mode 100644 index 000000000..0228f01af --- /dev/null +++ b/test/runtime/type/guard/type/ref.ts @@ -0,0 +1,44 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Type, CloneType } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TRef', () => { + // ---------------------------------------------------------------- + // Deprecated + // ---------------------------------------------------------------- + it('Should guard for Ref(Schema) 1', () => { + const T = Type.Number({ $id: 'T' }) + const R = Type.Ref(T) + Assert.IsTrue(TypeGuard.IsRef(R)) + Assert.IsTrue(typeof R['$ref'] === 'string') + }) + it('Should guard for Ref(Schema) 2', () => { + const T = Type.Number() + Assert.Throws(() => Type.Ref(T)) + }) + it('Should guard for Ref(Schema) 3', () => { + // @ts-ignore + const T = Type.Number({ $id: null }) + Assert.Throws(() => Type.Ref(T)) + }) + // ---------------------------------------------------------------- + // Standard + // ---------------------------------------------------------------- + it('Should guard for TRef', () => { + const T = Type.Number({ $id: 'T' }) + const R = TypeGuard.IsRef(Type.Ref('T')) + Assert.IsTrue(R) + }) + it('Should not guard for TRef', () => { + const R = TypeGuard.IsRef(null) + Assert.IsFalse(R) + }) + it('Should not guard for TRef with invalid $ref', () => { + const T = Type.Number({ $id: 'T' }) + const S = CloneType(Type.Ref('T')) + // @ts-ignore + S.$ref = 1 + const R = TypeGuard.IsRef(S) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/guard/type/regexp.ts b/test/runtime/type/guard/type/regexp.ts new file mode 100644 index 000000000..759d39860 --- /dev/null +++ b/test/runtime/type/guard/type/regexp.ts @@ -0,0 +1,33 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TRegExp', () => { + it('Should guard for TRegExp 1', () => { + const T = Type.RegExp(/foo/, { $id: 'T' }) + Assert.IsTrue(TypeGuard.IsSchema(T)) + }) + it('Should guard for TRegExp 1', () => { + const T = Type.RegExp(/foo/, { $id: 'T' }) + Assert.IsTrue(TypeGuard.IsRegExp(T)) + }) + it('Should guard for TRegExp 2', () => { + const T = Type.RegExp('foo', { $id: 'T' }) + Assert.IsTrue(TypeGuard.IsRegExp(T)) + }) + it('Should not guard for TRegExp 1', () => { + // @ts-ignore + const T = Type.RegExp('foo', { $id: 1 }) + Assert.IsFalse(TypeGuard.IsRegExp(T)) + }) + it('Should guard for RegExp constraint 1', () => { + // @ts-ignore + const T = Type.RegExp('foo', { maxLength: '1' }) + Assert.IsFalse(TypeGuard.IsRegExp(T)) + }) + it('Should guard for RegExp constraint 2', () => { + // @ts-ignore + const T = Type.RegExp('foo', { minLength: '1' }) + Assert.IsFalse(TypeGuard.IsRegExp(T)) + }) +}) diff --git a/test/runtime/type/guard/type/required.ts b/test/runtime/type/guard/type/required.ts new file mode 100644 index 000000000..3c4869594 --- /dev/null +++ b/test/runtime/type/guard/type/required.ts @@ -0,0 +1,105 @@ +import { TypeGuard, TypeRegistry, Type, Kind, TransformKind } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TRequired', () => { + it('Should produce a valid TSchema', () => { + const T = Type.Required(Type.Object({ x: Type.Number() })) + Assert.IsTrue(TypeGuard.IsSchema(T)) + }) + it('Should support TUnsafe required properties with no Kind', () => { + const T = Type.Required(Type.Object({ x: Type.Optional(Type.Unsafe({ x: 1 })) })) + Assert.IsEqual(T.required, ['x']) + }) + it('Should support TUnsafe required properties with unknown Kind', () => { + const T = Type.Required(Type.Object({ x: Type.Optional(Type.Unsafe({ [Kind]: 'UnknownRequiredType', x: 1 })) })) + Assert.IsEqual(T.required, ['x']) + }) + it('Should support TUnsafe required properties with known Kind', () => { + TypeRegistry.Set('KnownRequiredType', () => true) + const T = Type.Required(Type.Object({ x: Type.Optional(Type.Unsafe({ [Kind]: 'KnownRequiredType', x: 1 })) })) + Assert.IsEqual(T.required, ['x']) + }) + it('Should support applying required to intersect', () => { + const A = Type.Object({ x: Type.Optional(Type.Number()) }) + const B = Type.Object({ y: Type.Optional(Type.Number()) }) + const I = Type.Intersect([A, B]) + const T = Type.Required(I) + Assert.IsEqual(T.allOf.length, 2) + Assert.IsEqual(T.allOf[0].required, ['x']) + Assert.IsEqual(T.allOf[1].required, ['y']) + }) + it('Should support applying required to union', () => { + const A = Type.Object({ x: Type.Optional(Type.Number()) }) + const B = Type.Object({ y: Type.Optional(Type.Number()) }) + const I = Type.Union([A, B]) + const T = Type.Required(I) + Assert.IsEqual(T.anyOf.length, 2) + Assert.IsEqual(T.anyOf[0].required, ['x']) + Assert.IsEqual(T.anyOf[1].required, ['y']) + }) + // ---------------------------------------------------------------- + // Discard + // ---------------------------------------------------------------- + it('Should override $id', () => { + const A = Type.Object({ x: Type.Number() }, { $id: 'A' }) + const T = Type.Required(A, { $id: 'T' }) + Assert.IsEqual(T.$id!, 'T') + }) + it('Should discard $id', () => { + const A = Type.Object({ x: Type.Number() }, { $id: 'A' }) + const T = Type.Required(A) + Assert.IsFalse('$id' in T) + }) + it('Should discard transform', () => { + const T = Type.Object({ + x: Type.Number(), + y: Type.String(), + }) + const S = Type.Transform(T) + .Decode((value) => value) + .Encode((value) => value) + const R = Type.Required(S) + Assert.IsFalse(TransformKind in R) + }) + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/980 + // ---------------------------------------------------------------- + it('Should override properties in source type', () => { + const A = Type.Object({ x: Type.Number() }, { title: 'A' }) + const B = Type.Required(A, { title: 'B' }) + Assert.IsEqual(A.title, 'A') + Assert.IsEqual(B.title, 'B') + }) + // ------------------------------------------------------------------ + // Intrinsic Passthough + // https://github.com/sinclairzx81/typebox/issues/1169 + // ------------------------------------------------------------------ + it('Should pass through on intrinsic types on union 1', () => { + const T = Type.Required( + Type.Union([ + Type.Number(), + Type.Object({ + x: Type.Optional(Type.Number()), + }), + ]), + ) + Assert.IsTrue(TypeGuard.IsUnion(T)) + Assert.IsTrue(TypeGuard.IsNumber(T.anyOf[0])) + Assert.IsTrue(TypeGuard.IsObject(T.anyOf[1])) + Assert.IsFalse(TypeGuard.IsOptional(T.anyOf[1].properties.x)) + }) + it('Should pass through on intrinsic types on union 2', () => { + const T = Type.Required( + Type.Union([ + Type.Literal(1), + Type.Object({ + x: Type.Optional(Type.Number()), + }), + ]), + ) + Assert.IsTrue(TypeGuard.IsUnion(T)) + Assert.IsTrue(TypeGuard.IsLiteral(T.anyOf[0])) + Assert.IsTrue(TypeGuard.IsObject(T.anyOf[1])) + Assert.IsFalse(TypeGuard.IsOptional(T.anyOf[1].properties.x)) + }) +}) diff --git a/test/runtime/type/guard/type/rest.ts b/test/runtime/type/guard/type/rest.ts new file mode 100644 index 000000000..8383c2b2d --- /dev/null +++ b/test/runtime/type/guard/type/rest.ts @@ -0,0 +1,59 @@ +import { Type, TypeGuard } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TRest', () => { + it('Should guard 1', () => { + // union never + const A = Type.String() + const B = Type.Union(Type.Rest(A)) + Assert.IsTrue(TypeGuard.IsNever(B)) + }) + it('Should guard 2', () => { + // intersect never + const A = Type.String() + const B = Type.Intersect(Type.Rest(A)) + Assert.IsTrue(TypeGuard.IsNever(B)) + }) + it('Should guard 3', () => { + // tuple + const A = Type.Tuple([Type.Number(), Type.String()]) + const B = Type.Union(Type.Rest(A)) + Assert.IsTrue(TypeGuard.IsUnion(B)) + Assert.IsEqual(B.anyOf.length, 2) + Assert.IsTrue(TypeGuard.IsNumber(B.anyOf[0])) + Assert.IsTrue(TypeGuard.IsString(B.anyOf[1])) + }) + it('Should guard 4', () => { + // tuple spread + const A = Type.Tuple([Type.Literal(1), Type.Literal(2)]) + const B = Type.Tuple([Type.Literal(3), Type.Literal(4)]) + const C = Type.Tuple([...Type.Rest(A), ...Type.Rest(B)]) + Assert.IsTrue(TypeGuard.IsTuple(C)) + Assert.IsEqual(C.items!.length, 4) + Assert.IsEqual(C.items![0].const, 1) + Assert.IsEqual(C.items![1].const, 2) + Assert.IsEqual(C.items![2].const, 3) + Assert.IsEqual(C.items![3].const, 4) + }) + it('Should guard 5', () => { + // union to intersect + const A = Type.Object({ x: Type.Number() }) + const B = Type.Object({ y: Type.String() }) + const C = Type.Union([A, B]) + const D = Type.Intersect(Type.Rest(C)) + Assert.IsTrue(TypeGuard.IsIntersect(D)) + Assert.IsEqual(D.allOf.length, 2) + Assert.IsTrue(TypeGuard.IsObject(D.allOf[0])) + Assert.IsTrue(TypeGuard.IsObject(D.allOf[1])) + }) + it('Should guard 6', () => { + // intersect to composite + const A = Type.Object({ x: Type.Number() }) + const B = Type.Object({ y: Type.String() }) + const C = Type.Intersect([A, B]) + const D = Type.Composite(Type.Rest(C)) + Assert.IsTrue(TypeGuard.IsObject(D)) + Assert.IsTrue(TypeGuard.IsNumber(D.properties.x)) + Assert.IsTrue(TypeGuard.IsString(D.properties.y)) + }) +}) diff --git a/test/runtime/type/guard/type/string.ts b/test/runtime/type/guard/type/string.ts new file mode 100644 index 000000000..42aae379b --- /dev/null +++ b/test/runtime/type/guard/type/string.ts @@ -0,0 +1,34 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TString', () => { + it('Should guard for TString', () => { + const R = TypeGuard.IsString(Type.String()) + Assert.IsTrue(R) + }) + it('Should not guard for TString', () => { + const R = TypeGuard.IsString(null) + Assert.IsFalse(R) + }) + it('Should not guard for TString with invalid $id', () => { + // @ts-ignore + const R = TypeGuard.IsString(Type.String({ $id: 1 })) + Assert.IsFalse(R) + }) + it('Should not guard for TString with invalid minLength', () => { + // @ts-ignore + const R = TypeGuard.IsString(Type.String({ minLength: '1' })) + Assert.IsFalse(R) + }) + it('Should not guard for TString with invalid maxLength', () => { + // @ts-ignore + const R = TypeGuard.IsString(Type.String({ maxLength: '1' })) + Assert.IsFalse(R) + }) + it('Should not guard for TString with invalid pattern', () => { + // @ts-ignore + const R = TypeGuard.IsString(Type.String({ pattern: 1 })) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/guard/type/symbol.ts b/test/runtime/type/guard/type/symbol.ts new file mode 100644 index 000000000..488e08334 --- /dev/null +++ b/test/runtime/type/guard/type/symbol.ts @@ -0,0 +1,19 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TSymbol', () => { + it('Should guard for TSymbol', () => { + const R = TypeGuard.IsSymbol(Type.Symbol()) + Assert.IsTrue(R) + }) + it('Should not guard for TSymbol', () => { + const R = TypeGuard.IsSymbol(null) + Assert.IsFalse(R) + }) + it('Should not guard for TSymbol with invalid $id', () => { + // @ts-ignore + const R = TypeGuard.IsSymbol(Type.Symbol({ $id: 1 })) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/guard/type/template-literal.ts b/test/runtime/type/guard/type/template-literal.ts new file mode 100644 index 000000000..f00db33c2 --- /dev/null +++ b/test/runtime/type/guard/type/template-literal.ts @@ -0,0 +1,82 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Type, CloneType, TemplateLiteralGenerate } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TTemplateLiteral', () => { + it('Should guard for empty TemplateLiteral', () => { + const R = TypeGuard.IsTemplateLiteral(Type.TemplateLiteral([])) + Assert.IsTrue(R) + }) + it('Should guard for TSchema', () => { + const R = TypeGuard.IsSchema(Type.TemplateLiteral([])) + Assert.IsTrue(R) + }) + it('Should guard for TemplateLiteral (TTemplateLiteral)', () => { + const T = Type.TemplateLiteral([Type.Literal('hello')]) + const R = TypeGuard.IsTemplateLiteral(Type.TemplateLiteral([T, Type.Literal('world')])) + Assert.IsTrue(R) + }) + it('Should guard for TemplateLiteral (TLiteral)', () => { + const R = TypeGuard.IsTemplateLiteral(Type.TemplateLiteral([Type.Literal('hello')])) + Assert.IsTrue(R) + }) + it('Should guard for TemplateLiteral (TString)', () => { + const R = TypeGuard.IsTemplateLiteral(Type.TemplateLiteral([Type.String()])) + Assert.IsTrue(R) + }) + it('Should guard for TemplateLiteral (TNumber)', () => { + const R = TypeGuard.IsTemplateLiteral(Type.TemplateLiteral([Type.Number()])) + Assert.IsTrue(R) + }) + it('Should guard for TemplateLiteral (TBoolean)', () => { + const R = TypeGuard.IsTemplateLiteral(Type.TemplateLiteral([Type.Boolean()])) + Assert.IsTrue(R) + }) + it('Should not guard for missing ^ expression prefix', () => { + const T = CloneType(Type.TemplateLiteral([Type.Literal('hello')])) + // @ts-ignore + T.pattern = T.pattern.slice(1) + Assert.IsFalse(TypeGuard.IsTemplateLiteral(T)) + }) + it('Should not guard for missing $ expression postfix', () => { + const T = CloneType(Type.TemplateLiteral([Type.Literal('hello')])) + // @ts-ignore + T.pattern = T.pattern.slice(0, T.pattern.length - 1) + Assert.IsFalse(TypeGuard.IsTemplateLiteral(T)) + }) + // ---------------------------------------------------------------- + // issue: https://github.com/sinclairzx81/typebox/issues/913 + // ---------------------------------------------------------------- + it('Should generate embedded template literal 1', () => { + const A = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B')])]) + const B = Type.TemplateLiteral([Type.Union([Type.Literal('X'), Type.Literal('Y')])]) + const L = Type.TemplateLiteral([Type.Literal('KEY'), A, B]) + const T = TemplateLiteralGenerate(L) + Assert.IsEqual(T, ['KEYAX', 'KEYAY', 'KEYBX', 'KEYBY']) + }) + it('Should generate embedded template literal 2', () => { + const A = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B')])]) + const B = Type.TemplateLiteral([Type.Union([Type.Literal('X'), Type.Literal('Y')])]) + const L = Type.TemplateLiteral([A, Type.Literal('KEY'), B]) + const T = TemplateLiteralGenerate(L) + Assert.IsEqual(T, ['AKEYX', 'AKEYY', 'BKEYX', 'BKEYY']) + }) + it('Should generate embedded template literal 3', () => { + const A = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B')])]) + const B = Type.TemplateLiteral([Type.Union([Type.Literal('X'), Type.Literal('Y')])]) + const L = Type.TemplateLiteral([A, B, Type.Literal('KEY')]) + const T = TemplateLiteralGenerate(L) + Assert.IsEqual(T, ['AXKEY', 'AYKEY', 'BXKEY', 'BYKEY']) + }) + it('Should map embedded template literal', () => { + const A = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B')])]) + const B = Type.TemplateLiteral([Type.Union([Type.Literal('X'), Type.Literal('Y')])]) + const L = Type.TemplateLiteral([Type.Literal('KEY'), A, B]) + const T = Type.Mapped(L, (K) => Type.Null()) + Assert.IsTrue(TypeGuard.IsObject(T)) + Assert.IsTrue(TypeGuard.IsNull(T.properties.KEYAX)) + Assert.IsTrue(TypeGuard.IsNull(T.properties.KEYAY)) + Assert.IsTrue(TypeGuard.IsNull(T.properties.KEYBX)) + Assert.IsTrue(TypeGuard.IsNull(T.properties.KEYBY)) + }) +}) diff --git a/test/runtime/type/guard/type/this.ts b/test/runtime/type/guard/type/this.ts new file mode 100644 index 000000000..800018219 --- /dev/null +++ b/test/runtime/type/guard/type/this.ts @@ -0,0 +1,22 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TThis', () => { + it('Should guard for TThis', () => { + Type.Recursive((This) => { + const R = TypeGuard.IsThis(This) + Assert.IsTrue(R) + return Type.Object({ nodes: Type.Array(This) }) + }) + }) + it('Should guard for TThis with invalid $ref', () => { + Type.Recursive((This) => { + // @ts-ignore + This.$ref = 1 + const R = TypeGuard.IsThis(This) + Assert.IsFalse(R) + return Type.Object({ nodes: Type.Array(This) }) + }) + }) +}) diff --git a/test/runtime/type/guard/type/tuple.ts b/test/runtime/type/guard/type/tuple.ts new file mode 100644 index 000000000..b47ecde3f --- /dev/null +++ b/test/runtime/type/guard/type/tuple.ts @@ -0,0 +1,23 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TTuple', () => { + it('Should guard for TTuple', () => { + const R = TypeGuard.IsTuple(Type.Tuple([Type.Number(), Type.Number()])) + Assert.IsTrue(R) + }) + it('Should not guard for TTuple', () => { + const R = TypeGuard.IsTuple(null) + Assert.IsFalse(R) + }) + it('Should not guard for TTuple with invalid $id', () => { + // @ts-ignore + const R = TypeGuard.IsTuple(Type.Tuple([Type.Number(), Type.Number()], { $id: 1 })) + Assert.IsFalse(R) + }) + it('Should not guard for TTuple with invalid Items', () => { + const R = TypeGuard.IsTuple(Type.Tuple([Type.Number(), {} as any])) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/guard/type/uint8array.ts b/test/runtime/type/guard/type/uint8array.ts new file mode 100644 index 000000000..ae0b88cfd --- /dev/null +++ b/test/runtime/type/guard/type/uint8array.ts @@ -0,0 +1,29 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TUint8Array', () => { + it('Should guard for TUint8Array', () => { + const R = TypeGuard.IsUint8Array(Type.Uint8Array()) + Assert.IsTrue(R) + }) + it('Should not guard for TUint8Array', () => { + const R = TypeGuard.IsUint8Array(null) + Assert.IsFalse(R) + }) + it('Should not guard for TUint8Array with invalid $id', () => { + // @ts-ignore + const R = TypeGuard.IsUint8Array(Type.Uint8Array({ $id: 1 })) + Assert.IsFalse(R) + }) + it('Should not guard for TUint8Array with invalid minByteLength', () => { + // @ts-ignore + const R = TypeGuard.IsUint8Array(Type.Uint8Array({ minByteLength: '1' })) + Assert.IsFalse(R) + }) + it('Should not guard for TUint8Array with invalid maxByteLength', () => { + // @ts-ignore + const R = TypeGuard.IsUint8Array(Type.Uint8Array({ maxByteLength: '1' })) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/guard/type/uncapitalize.ts b/test/runtime/type/guard/type/uncapitalize.ts new file mode 100644 index 000000000..61a60e908 --- /dev/null +++ b/test/runtime/type/guard/type/uncapitalize.ts @@ -0,0 +1,33 @@ +import { TypeGuard, Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/Uncapitalize', () => { + it('Should guard for Uncapitalize 1', () => { + const T = Type.Uncapitalize(Type.Literal('HELLO'), { $id: 'hello', foo: 1 }) + Assert.IsTrue(TypeGuard.IsLiteral(T)) + Assert.IsEqual(T.const, 'hELLO') + Assert.IsEqual(T.$id, 'hello') + Assert.IsEqual(T.foo, 1) + }) + it('Should guard for Uncapitalize 2', () => { + const T = Type.Uncapitalize(Type.Literal('HELLO')) + Assert.IsTrue(TypeGuard.IsLiteral(T)) + Assert.IsEqual(T.const, 'hELLO') + }) + it('Should guard for Uncapitalize 3', () => { + const T = Type.Uncapitalize(Type.Union([Type.Literal('HELLO'), Type.Literal('WORLD')])) + Assert.IsTrue(TypeGuard.IsUnion(T)) + Assert.IsEqual(T.anyOf[0].const, 'hELLO') + Assert.IsEqual(T.anyOf[1].const, 'wORLD') + }) + it('Should guard for Uncapitalize 4', () => { + const T = Type.Uncapitalize(Type.TemplateLiteral('HELLO${0|1}')) + Assert.IsTrue(TypeGuard.IsTemplateLiteral(T)) + Assert.IsEqual(T.pattern, '^(hELLO0|hELLO1)$') + }) + it('Should guard for Uncapitalize 5', () => { + const T = Type.Uncapitalize(Type.TemplateLiteral([Type.Literal('HELLO'), Type.Union([Type.Literal(0), Type.Literal(1)])])) + Assert.IsTrue(TypeGuard.IsTemplateLiteral(T)) + Assert.IsEqual(T.pattern, '^(hELLO0|hELLO1)$') + }) +}) diff --git a/test/runtime/type/guard/type/undefined.ts b/test/runtime/type/guard/type/undefined.ts new file mode 100644 index 000000000..ccebb9551 --- /dev/null +++ b/test/runtime/type/guard/type/undefined.ts @@ -0,0 +1,19 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TUndefined', () => { + it('Should guard for TUndefined', () => { + const R = TypeGuard.IsUndefined(Type.Undefined()) + Assert.IsTrue(R) + }) + it('Should not guard for TUndefined', () => { + const R = TypeGuard.IsUndefined(null) + Assert.IsFalse(R) + }) + it('Should not guard for TUndefined with invalid $id', () => { + // @ts-ignore + const R = TypeGuard.IsUndefined(Type.Undefined({ $id: 1 })) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/guard/type/union.ts b/test/runtime/type/guard/type/union.ts new file mode 100644 index 000000000..fb5ca4462 --- /dev/null +++ b/test/runtime/type/guard/type/union.ts @@ -0,0 +1,86 @@ +import { TSchema, TypeGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TUnion', () => { + it('Should guard for TUnion', () => { + const R = TypeGuard.IsUnion( + Type.Union([ + Type.Object({ + x: Type.Number(), + }), + Type.Object({ + y: Type.Number(), + }), + ]), + ) + Assert.IsTrue(R) + }) + it('Should not guard for TUnion', () => { + const R = TypeGuard.IsUnion(null) + Assert.IsFalse(R) + }) + it('Should guard for TUnion with invalid $id', () => { + const R = TypeGuard.IsUnion( + // @ts-ignore + Type.Union( + [ + Type.Object({ + x: Type.Number(), + }), + Type.Object({ + y: Type.Number(), + }), + ], + { + // @ts-ignore + $id: 1, + }, + ), + ) + Assert.IsFalse(R) + }) + it('Should not guard for TUnion with invalid variant', () => { + const R = TypeGuard.IsUnion( + Type.Union([ + Type.Object({ + x: Type.Number(), + }), + {} as TSchema, + ]), + ) + Assert.IsFalse(R) + }) + it('Should not guard for TUnion with invalid object variant', () => { + const R = TypeGuard.IsUnion( + Type.Union([ + Type.Object({ + x: Type.Number(), + }), + Type.Object({ + y: {} as any, + }), + ]), + ) + Assert.IsFalse(R) + }) + it('Transform: Should transform to never for zero length union', () => { + const T = Type.Union([]) + const R = TypeGuard.IsNever(T) + Assert.IsTrue(R) + }) + it('Transform: Should unwrap union type for array of length === 1', () => { + const T = Type.Union([Type.String()]) + const R = TypeGuard.IsString(T) + Assert.IsTrue(R) + }) + it('Transform: Should retain union if array length > 1', () => { + const T = Type.Union([Type.String(), Type.Number()]) + const R1 = TypeGuard.IsUnion(T) + const R2 = TypeGuard.IsString(T.anyOf[0]) + const R3 = TypeGuard.IsNumber(T.anyOf[1]) + Assert.IsTrue(R1) + Assert.IsTrue(R2) + Assert.IsTrue(R3) + }) +}) diff --git a/test/runtime/type/guard/type/unknown.ts b/test/runtime/type/guard/type/unknown.ts new file mode 100644 index 000000000..7cc9a29ae --- /dev/null +++ b/test/runtime/type/guard/type/unknown.ts @@ -0,0 +1,19 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TUnknown', () => { + it('Should guard for TUnknown', () => { + const R = TypeGuard.IsUnknown(Type.Unknown()) + Assert.IsTrue(R) + }) + it('Should not guard for TUnknown', () => { + const R = TypeGuard.IsUnknown(null) + Assert.IsFalse(R) + }) + it('Should not guard for TUnknown with invalid $id', () => { + // @ts-ignore + const R = TypeGuard.IsUnknown(Type.Unknown({ $id: 1 })) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/guard/type/unsafe.ts b/test/runtime/type/guard/type/unsafe.ts new file mode 100644 index 000000000..1b88d9ddc --- /dev/null +++ b/test/runtime/type/guard/type/unsafe.ts @@ -0,0 +1,33 @@ +import { Kind, TypeGuard, TypeRegistry } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TUnsafe', () => { + it('Should guard raw TUnsafe', () => { + const T = Type.Unsafe({ x: 1 }) + const R = TypeGuard.IsUnsafe(T) + Assert.IsTrue(R) + }) + it('Should guard raw TUnsafe as TSchema', () => { + const T = Type.Unsafe({ x: 1 }) + const R = TypeGuard.IsSchema(T) + Assert.IsTrue(R) + }) + it('Should guard override TUnsafe as TSchema when registered', () => { + TypeRegistry.Set('UnsafeType', () => true) + const T = Type.Unsafe({ [Kind]: 'UnsafeType' }) + const R = TypeGuard.IsSchema(T) + Assert.IsTrue(R) + TypeRegistry.Delete('UnsafeType') + }) + it('Should not guard TUnsafe with unregistered kind', () => { + const T = Type.Unsafe({ [Kind]: 'UnsafeType' }) + const R = TypeGuard.IsUnsafe(T) + Assert.IsFalse(R) + }) + it('Should not guard for TString', () => { + const T = Type.String() + const R = TypeGuard.IsUnsafe(T) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/guard/type/uppercase.ts b/test/runtime/type/guard/type/uppercase.ts new file mode 100644 index 000000000..ba10aa46f --- /dev/null +++ b/test/runtime/type/guard/type/uppercase.ts @@ -0,0 +1,33 @@ +import { TypeGuard, Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/Uppercase', () => { + it('Should guard for Uppercase 1', () => { + const T = Type.Uppercase(Type.Literal('hello'), { $id: 'hello', foo: 1 }) + Assert.IsTrue(TypeGuard.IsLiteral(T)) + Assert.IsEqual(T.const, 'HELLO') + Assert.IsEqual(T.$id, 'hello') + Assert.IsEqual(T.foo, 1) + }) + it('Should guard for Uppercase 2', () => { + const T = Type.Uppercase(Type.Literal('hello')) + Assert.IsTrue(TypeGuard.IsLiteral(T)) + Assert.IsEqual(T.const, 'HELLO') + }) + it('Should guard for Uppercase 3', () => { + const T = Type.Uppercase(Type.Union([Type.Literal('hello'), Type.Literal('world')])) + Assert.IsTrue(TypeGuard.IsUnion(T)) + Assert.IsEqual(T.anyOf[0].const, 'HELLO') + Assert.IsEqual(T.anyOf[1].const, 'WORLD') + }) + it('Should guard for Uppercase 4', () => { + const T = Type.Uppercase(Type.TemplateLiteral('hello${0|1}')) + Assert.IsTrue(TypeGuard.IsTemplateLiteral(T)) + Assert.IsEqual(T.pattern, '^(HELLO0|HELLO1)$') + }) + it('Should guard for Uppercase 5', () => { + const T = Type.Uppercase(Type.TemplateLiteral([Type.Literal('hello'), Type.Union([Type.Literal(0), Type.Literal(1)])])) + Assert.IsTrue(TypeGuard.IsTemplateLiteral(T)) + Assert.IsEqual(T.pattern, '^(HELLO0|HELLO1)$') + }) +}) diff --git a/test/runtime/type/guard/type/void.ts b/test/runtime/type/guard/type/void.ts new file mode 100644 index 000000000..f9320c6c4 --- /dev/null +++ b/test/runtime/type/guard/type/void.ts @@ -0,0 +1,19 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../../assert/index' + +describe('guard/type/TVoid', () => { + it('Should guard for TVoid', () => { + const R = TypeGuard.IsVoid(Type.Void()) + Assert.IsTrue(R) + }) + it('Should not guard for TVoid', () => { + const R = TypeGuard.IsVoid(null) + Assert.IsFalse(R) + }) + it('Should not guard for TVoid with invalid $id', () => { + // @ts-ignore + const R = TypeGuard.IsVoid(Type.Void({ $id: 1 })) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/guard/value/guard.ts b/test/runtime/type/guard/value/guard.ts new file mode 100644 index 000000000..b58df9bea --- /dev/null +++ b/test/runtime/type/guard/value/guard.ts @@ -0,0 +1,187 @@ +import { Assert } from '../../../assert/index' +import { ValueGuard } from '@sinclair/typebox' + +describe('type/ValueGuard', () => { + // ----------------------------------------------------- + // IsSymbol + // ----------------------------------------------------- + it('Should guard symbol 1', () => { + const R = ValueGuard.IsSymbol(Symbol()) + Assert.IsTrue(R) + }) + it('Should guard symbol 2', () => { + const R = ValueGuard.IsSymbol({}) + Assert.IsFalse(R) + }) + // ----------------------------------------------------- + // IsNull + // ----------------------------------------------------- + it('Should guard null 1', () => { + const R = ValueGuard.IsNull(null) + Assert.IsTrue(R) + }) + it('Should guard null 2', () => { + const R = ValueGuard.IsNull({}) + Assert.IsFalse(R) + }) + // ----------------------------------------------------- + // IsUndefined + // ----------------------------------------------------- + it('Should guard undefined 1', () => { + const R = ValueGuard.IsUndefined(undefined) + Assert.IsTrue(R) + }) + it('Should guard undefined 2', () => { + const R = ValueGuard.IsUndefined({}) + Assert.IsFalse(R) + }) + // ----------------------------------------------------- + // IsBigInt + // ----------------------------------------------------- + it('Should guard bigint 1', () => { + const R = ValueGuard.IsBigInt(1n) + Assert.IsTrue(R) + }) + it('Should guard bigint 2', () => { + const R = ValueGuard.IsBigInt(1) + Assert.IsFalse(R) + }) + it('Should guard bigint 3', () => { + const R = ValueGuard.IsBigInt('123') + Assert.IsFalse(R) + }) + // ----------------------------------------------------- + // IsNumber + // ----------------------------------------------------- + it('Should guard number 1', () => { + const R = ValueGuard.IsNumber(1) + Assert.IsTrue(R) + }) + it('Should guard number 2', () => { + const R = ValueGuard.IsNumber(3.14) + Assert.IsTrue(R) + }) + it('Should guard number 3', () => { + const R = ValueGuard.IsNumber('') + Assert.IsFalse(R) + }) + it('Should guard number 4', () => { + const R = ValueGuard.IsNumber(NaN) + Assert.IsTrue(R) + }) + // ----------------------------------------------------- + // IsString + // ----------------------------------------------------- + it('Should guard string 1', () => { + const R = ValueGuard.IsString('') + Assert.IsTrue(R) + }) + it('Should guard string 2', () => { + const R = ValueGuard.IsString(true) + Assert.IsFalse(R) + }) + // ----------------------------------------------------- + // IsBoolean + // ----------------------------------------------------- + it('Should guard boolean 1', () => { + const R = ValueGuard.IsBoolean(true) + Assert.IsTrue(R) + }) + it('Should guard boolean 2', () => { + const R = ValueGuard.IsBoolean(1) + Assert.IsFalse(R) + }) + // ----------------------------------------------------- + // IsObject + // ----------------------------------------------------- + it('Should guard object 1', () => { + const R = ValueGuard.IsObject({}) + Assert.IsTrue(R) + }) + it('Should guard object 2', () => { + const R = ValueGuard.IsObject(1) + Assert.IsFalse(R) + }) + it('Should guard object 3', () => { + const R = ValueGuard.IsObject([]) + Assert.IsTrue(R) + }) + // ----------------------------------------------------- + // IsArray + // ----------------------------------------------------- + it('Should guard array 1', () => { + const R = ValueGuard.IsArray([]) + Assert.IsTrue(R) + }) + it('Should guard array 2', () => { + const R = ValueGuard.IsArray({}) + Assert.IsFalse(R) + }) + // ----------------------------------------------------- + // IsFunction + // ----------------------------------------------------- + it('Should guard function 1', () => { + const R = ValueGuard.IsFunction(function () {}) + Assert.IsTrue(R) + }) + it('Should guard function 2', () => { + const R = ValueGuard.IsFunction({}) + Assert.IsFalse(R) + }) + // ----------------------------------------------------- + // IsAsyncIterator + // ----------------------------------------------------- + it('Should guard async iterator 1', () => { + const R = ValueGuard.IsAsyncIterator((async function* (): any {})()) + Assert.IsTrue(R) + }) + it('Should guard async iterator 2', () => { + const R = ValueGuard.IsAsyncIterator((function* (): any {})()) + Assert.IsFalse(R) + }) + // ----------------------------------------------------- + // IsAsyncIterator + // ----------------------------------------------------- + it('Should guard iterator 1', () => { + const R = ValueGuard.IsIterator((function* (): any {})()) + Assert.IsTrue(R) + }) + it('Should guard iterator 2', () => { + const R = ValueGuard.IsIterator((async function* (): any {})()) + Assert.IsFalse(R) + }) + it('Should guard iterator 3', () => { + const R = ValueGuard.IsIterator([]) + Assert.IsFalse(R) + }) + it('Should guard iterator 4', () => { + const R = ValueGuard.IsIterator(new Uint8Array()) + Assert.IsFalse(R) + }) + // ----------------------------------------------------- + // Date + // ----------------------------------------------------- + it('Should guard date 1', () => { + const R = ValueGuard.IsDate(new Date()) + Assert.IsTrue(R) + }) + it('Should guard date 2', () => { + const R = ValueGuard.IsDate({}) + Assert.IsFalse(R) + }) + // ----------------------------------------------------- + // Date + // ----------------------------------------------------- + it('Should guard uint8array 1', () => { + const R = ValueGuard.IsUint8Array(new Uint8Array()) + Assert.IsTrue(R) + }) + it('Should guard uint8array 2', () => { + const R = ValueGuard.IsUint8Array({}) + Assert.IsFalse(R) + }) + it('Should guard uint8array 2', () => { + const R = ValueGuard.IsUint8Array([]) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/guard/value/index.ts b/test/runtime/type/guard/value/index.ts new file mode 100644 index 000000000..4f2ca894f --- /dev/null +++ b/test/runtime/type/guard/value/index.ts @@ -0,0 +1 @@ +import './guard' diff --git a/test/runtime/type/index.ts b/test/runtime/type/index.ts new file mode 100644 index 000000000..2551b5ae8 --- /dev/null +++ b/test/runtime/type/index.ts @@ -0,0 +1,9 @@ +import './clone/index' +import './extends/index' +import './guard/index' +import './intrinsic/index' +import './normalize/index' +import './options/index' +import './registry/index' +import './sets/index' +import './template-literal/index' diff --git a/test/runtime/type/intrinsic/index.ts b/test/runtime/type/intrinsic/index.ts new file mode 100644 index 000000000..8bb9e079c --- /dev/null +++ b/test/runtime/type/intrinsic/index.ts @@ -0,0 +1 @@ +import './intrinsic' diff --git a/test/runtime/type/intrinsic/intrinsic.ts b/test/runtime/type/intrinsic/intrinsic.ts new file mode 100644 index 000000000..a6a18f34e --- /dev/null +++ b/test/runtime/type/intrinsic/intrinsic.ts @@ -0,0 +1,164 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Intrinsic } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('type/intrinsic/Intrinsic', () => { + // ---------------------------------------------------- + // Passthrough + // ---------------------------------------------------- + it('Should passthrough 1', () => { + const T = Intrinsic(Type.String(), 'Capitalize') + Assert.IsTrue(TypeGuard.IsString(T)) + }) + it('Should passthrough 2', () => { + const T = Intrinsic(Type.Number(), 'Capitalize') + Assert.IsTrue(TypeGuard.IsNumber(T)) + }) + it('Should passthrough 3', () => { + const T = Intrinsic( + Type.Object({ + x: Type.Number(), + }), + 'Capitalize', + ) + Assert.IsTrue(TypeGuard.IsObject(T)) + }) + // ---------------------------------------------------- + // Partial Passthrough + // ---------------------------------------------------- + it('Should partial passthrough 3', () => { + const A = Type.Object({ x: Type.Number() }) + const B = Type.Literal('hello') + const U = Type.Union([A, B]) + const T = Intrinsic(U, 'Capitalize') + Assert.IsTrue(TypeGuard.IsUnion(T)) + Assert.IsTrue(TypeGuard.IsObject(T.anyOf[0])) + Assert.IsTrue(TypeGuard.IsLiteral(T.anyOf[1])) + Assert.IsEqual(T.anyOf[1].const, 'Hello') + }) + // ---------------------------------------------------- + // Mode: Literal + // ---------------------------------------------------- + it('Should map literal: Capitalize', () => { + const T = Intrinsic(Type.Literal('hello'), 'Capitalize') + Assert.IsTrue(TypeGuard.IsLiteral(T)) + Assert.IsEqual(T.const, 'Hello') + }) + it('Should map literal: Uncapitalize', () => { + const T = Intrinsic(Type.Literal('HELLO'), 'Uncapitalize') + Assert.IsTrue(TypeGuard.IsLiteral(T)) + Assert.IsEqual(T.const, 'hELLO') + }) + it('Should map literal: Uppercase', () => { + const T = Intrinsic(Type.Literal('hello'), 'Uppercase') + Assert.IsTrue(TypeGuard.IsLiteral(T)) + Assert.IsEqual(T.const, 'HELLO') + }) + it('Should map literal: Lowercase', () => { + const T = Intrinsic(Type.Literal('HELLO'), 'Lowercase') + Assert.IsTrue(TypeGuard.IsLiteral(T)) + Assert.IsEqual(T.const, 'hello') + }) + // ---------------------------------------------------- + // Mode: Literal Union + // ---------------------------------------------------- + it('Should map literal union: Capitalize', () => { + const T = Intrinsic(Type.Union([Type.Literal('hello'), Type.Literal('world')]), 'Capitalize') + Assert.IsTrue(TypeGuard.IsUnion(T)) + Assert.IsEqual(T.anyOf[0].const, 'Hello') + Assert.IsEqual(T.anyOf[1].const, 'World') + }) + it('Should map literal union: Uncapitalize', () => { + const T = Intrinsic(Type.Union([Type.Literal('Hello'), Type.Literal('World')]), 'Uncapitalize') + Assert.IsTrue(TypeGuard.IsUnion(T)) + Assert.IsEqual(T.anyOf[0].const, 'hello') + Assert.IsEqual(T.anyOf[1].const, 'world') + }) + it('Should map literal union: Uppercase', () => { + const T = Intrinsic(Type.Union([Type.Literal('hello'), Type.Literal('world')]), 'Uppercase') + Assert.IsTrue(TypeGuard.IsUnion(T)) + Assert.IsEqual(T.anyOf[0].const, 'HELLO') + Assert.IsEqual(T.anyOf[1].const, 'WORLD') + }) + it('Should map literal union: Lowercase', () => { + const T = Intrinsic(Type.Union([Type.Literal('HELLO'), Type.Literal('WORLD')]), 'Lowercase') + Assert.IsTrue(TypeGuard.IsUnion(T)) + Assert.IsEqual(T.anyOf[0].const, 'hello') + Assert.IsEqual(T.anyOf[1].const, 'world') + }) + // ---------------------------------------------------- + // Mode: TemplateLiteral + // ---------------------------------------------------- + it('Should map template literal: Capitalize', () => { + const T = Intrinsic(Type.TemplateLiteral('hello${1|2}world'), 'Capitalize') + Assert.IsTrue(TypeGuard.IsTemplateLiteral(T)) + Assert.IsEqual(T.pattern, '^(Hello1world|Hello2world)$') + }) + it('Should map template literal: Uncapitalize', () => { + const T = Intrinsic(Type.TemplateLiteral('HELLO${1|2}WORLD'), 'Uncapitalize') + Assert.IsTrue(TypeGuard.IsTemplateLiteral(T)) + Assert.IsEqual(T.pattern, '^(hELLO1WORLD|hELLO2WORLD)$') + }) + it('Should map template literal: Uppercase', () => { + const T = Intrinsic(Type.TemplateLiteral('hello${1|2}world'), 'Uppercase') + Assert.IsTrue(TypeGuard.IsTemplateLiteral(T)) + Assert.IsEqual(T.pattern, '^(HELLO1WORLD|HELLO2WORLD)$') + }) + it('Should map template literal: Lowercase', () => { + const T = Intrinsic(Type.TemplateLiteral('HELLO${1|2}WORLD'), 'Lowercase') + Assert.IsTrue(TypeGuard.IsTemplateLiteral(T)) + Assert.IsEqual(T.pattern, '^(hello1world|hello2world)$') + }) + // ---------------------------------------------------- + // Mode: TemplateLiteral Numeric + // ---------------------------------------------------- + it('Should map template literal numeric: Capitalize', () => { + const T = Intrinsic(Type.TemplateLiteral([Type.Literal('hello'), Type.Union([Type.Literal(1), Type.Literal(2)])]), 'Capitalize') + Assert.IsTrue(TypeGuard.IsTemplateLiteral(T)) + Assert.IsEqual(T.pattern, '^(Hello1|Hello2)$') + }) + it('Should map template literal numeric: Uncapitalize', () => { + const T = Intrinsic(Type.TemplateLiteral([Type.Literal('HELLO'), Type.Union([Type.Literal(1), Type.Literal(2)])]), 'Uncapitalize') + Assert.IsTrue(TypeGuard.IsTemplateLiteral(T)) + Assert.IsEqual(T.pattern, '^(hELLO1|hELLO2)$') + }) + it('Should map template literal numeric: Uppercase', () => { + const T = Intrinsic(Type.TemplateLiteral([Type.Literal('hello'), Type.Union([Type.Literal(1), Type.Literal(2)])]), 'Uppercase') + Assert.IsTrue(TypeGuard.IsTemplateLiteral(T)) + Assert.IsEqual(T.pattern, '^(HELLO1|HELLO2)$') + }) + it('Should map template literal numeric: Lowercase', () => { + const T = Intrinsic(Type.TemplateLiteral([Type.Literal('HELLO'), Type.Union([Type.Literal(1), Type.Literal(2)])]), 'Lowercase') + Assert.IsTrue(TypeGuard.IsTemplateLiteral(T)) + Assert.IsEqual(T.pattern, '^(hello1|hello2)$') + }) + // ---------------------------------------------------- + // Mode: TemplateLiteral Patterns + // ---------------------------------------------------- + it('Should map template literal patterns 1', () => { + const T = Intrinsic(Type.TemplateLiteral('HELLO${string}WORLD'), 'Lowercase') + Assert.IsTrue(TypeGuard.IsTemplateLiteral(T)) + Assert.IsEqual(T.pattern, '^hello(.*)world$') + }) + it('Should map template literal patterns 2', () => { + const T = Intrinsic(Type.TemplateLiteral('HELLO${number}WORLD'), 'Lowercase') + Assert.IsTrue(TypeGuard.IsTemplateLiteral(T)) + Assert.IsEqual(T.pattern, '^hello(0|[1-9][0-9]*)world$') + }) + it('Should map template literal patterns 3', () => { + const T = Intrinsic(Type.TemplateLiteral('${number}${string}'), 'Lowercase') + Assert.IsTrue(TypeGuard.IsTemplateLiteral(T)) + Assert.IsEqual(T.pattern, '^(0|[1-9][0-9]*)(.*)$') + }) + it('Should map template literal patterns 3', () => { + const T = Intrinsic(Type.TemplateLiteral('${number}HELLO${string}'), 'Lowercase') + Assert.IsTrue(TypeGuard.IsTemplateLiteral(T)) + Assert.IsEqual(T.pattern, '^(0|[1-9][0-9]*)hello(.*)$') + }) + it('Should map template literal patterns 3', () => { + const T = Intrinsic(Type.TemplateLiteral('${string|number}HELLO'), 'Lowercase') + Assert.IsTrue(TypeGuard.IsTemplateLiteral(T)) + Assert.IsEqual(T.pattern, '^(stringhello|numberhello)$') + }) +}) diff --git a/test/runtime/type/normalize/exclude.ts b/test/runtime/type/normalize/exclude.ts new file mode 100644 index 000000000..53fad9c4d --- /dev/null +++ b/test/runtime/type/normalize/exclude.ts @@ -0,0 +1,21 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('type/normalize/Exclude', () => { + it('Normalize 1', () => { + const T = Type.Exclude(Type.Union([Type.String(), Type.Number(), Type.Boolean()]), Type.String()) + const R = TypeGuard.IsUnion(T) + Assert.IsTrue(R) + }) + it('Normalize 2', () => { + const T = Type.Exclude(Type.Union([Type.String(), Type.Number()]), Type.String()) + const R = TypeGuard.IsNumber(T) + Assert.IsTrue(R) + }) + it('Normalize 3', () => { + const T = Type.Exclude(Type.Union([Type.String()]), Type.String()) + const R = TypeGuard.IsNever(T) + Assert.IsTrue(R) + }) +}) diff --git a/test/runtime/type/normalize/extract.ts b/test/runtime/type/normalize/extract.ts new file mode 100644 index 000000000..196ee0f4a --- /dev/null +++ b/test/runtime/type/normalize/extract.ts @@ -0,0 +1,31 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('type/normalize/Extract', () => { + it('Normalize 1', () => { + const T = Type.Extract(Type.Union([Type.String(), Type.Number(), Type.Boolean()]), Type.Union([Type.String(), Type.Number()])) + const R = TypeGuard.IsUnion(T) + Assert.IsTrue(R) + }) + it('Normalize 2', () => { + const T = Type.Extract(Type.Union([Type.String(), Type.Number(), Type.Boolean()]), Type.String()) + const R = TypeGuard.IsString(T) + Assert.IsTrue(R) + }) + it('Normalize 3', () => { + const T = Type.Extract(Type.Union([Type.String(), Type.Number()]), Type.String()) + const R = TypeGuard.IsString(T) + Assert.IsTrue(R) + }) + it('Normalize 4', () => { + const T = Type.Extract(Type.Union([Type.String()]), Type.String()) + const R = TypeGuard.IsString(T) + Assert.IsTrue(R) + }) + it('Normalize 5', () => { + const T = Type.Extract(Type.Union([]), Type.String()) + const R = TypeGuard.IsNever(T) + Assert.IsTrue(R) + }) +}) diff --git a/test/runtime/type/normalize/index.ts b/test/runtime/type/normalize/index.ts new file mode 100644 index 000000000..051f3a9bb --- /dev/null +++ b/test/runtime/type/normalize/index.ts @@ -0,0 +1,6 @@ +import './exclude' +import './extract' +import './intersect' +import './indexed' +import './record' +import './union' diff --git a/test/runtime/type/normalize/indexed.ts b/test/runtime/type/normalize/indexed.ts new file mode 100644 index 000000000..543f9b580 --- /dev/null +++ b/test/runtime/type/normalize/indexed.ts @@ -0,0 +1,140 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('type/normalize/Index', () => { + // --------------------------------------------------------- + // Array + // --------------------------------------------------------- + it('Normalize Array 1', () => { + const T = Type.Array(Type.String()) + const I = Type.Index(T, Type.Number()) + Assert.IsTrue(TypeGuard.IsString(I)) + }) + // --------------------------------------------------------- + // Tuple + // --------------------------------------------------------- + it('Normalize Tuple 1', () => { + const T = Type.Tuple([Type.String(), Type.Number()]) + const I = Type.Index(T, Type.Number()) + Assert.IsTrue(TypeGuard.IsUnion(I)) + Assert.IsTrue(TypeGuard.IsString(I.anyOf[0])) + Assert.IsTrue(TypeGuard.IsNumber(I.anyOf[1])) + }) + it('Normalize Tuple 2', () => { + const T = Type.Tuple([Type.String(), Type.Number()]) + const I = Type.Index(T, [0, 1]) + Assert.IsTrue(TypeGuard.IsUnion(I)) + Assert.IsTrue(TypeGuard.IsString(I.anyOf[0])) + Assert.IsTrue(TypeGuard.IsNumber(I.anyOf[1])) + }) + it('Normalize Tuple 3', () => { + const T = Type.Tuple([Type.String(), Type.Number()]) + const I = Type.Index(T, ['0', '1']) + Assert.IsTrue(TypeGuard.IsUnion(I)) + Assert.IsTrue(TypeGuard.IsString(I.anyOf[0])) + Assert.IsTrue(TypeGuard.IsNumber(I.anyOf[1])) + }) + it('Normalize Tuple 4', () => { + const T = Type.Tuple([Type.String(), Type.Number()]) + const I = Type.Index(T, ['0']) + Assert.IsTrue(TypeGuard.IsString(I)) + }) + it('Normalize Tuple 5', () => { + const T = Type.Tuple([Type.String(), Type.Number()]) + const I = Type.Index(T, ['1']) + Assert.IsTrue(TypeGuard.IsNumber(I)) + }) + // --------------------------------------------------------- + // Intersect + // --------------------------------------------------------- + it('Normalize Intersect 1', () => { + const T = Type.Intersect([Type.Object({ x: Type.Optional(Type.String()) }), Type.Object({ x: Type.Optional(Type.String()) })]) + const I = Type.Index(T, ['x']) + Assert.IsTrue(TypeGuard.IsOptional(I)) + Assert.IsTrue(TypeGuard.IsIntersect(I)) + Assert.IsTrue(TypeGuard.IsString(I.allOf[0])) + Assert.IsTrue(TypeGuard.IsString(I.allOf[1])) + }) + it('Normalize Intersect 2', () => { + const T = Type.Intersect([Type.Object({ x: Type.Optional(Type.String()) }), Type.Object({ x: Type.String() })]) + const I = Type.Index(T, ['x']) + Assert.IsFalse(TypeGuard.IsOptional(I)) + Assert.IsTrue(TypeGuard.IsIntersect(I)) + Assert.IsTrue(TypeGuard.IsString(I.allOf[0])) + Assert.IsTrue(TypeGuard.IsString(I.allOf[1])) + }) + it('Normalize Intersect 3', () => { + const T = Type.Intersect([Type.Object({ x: Type.String() }), Type.Object({ x: Type.String() })]) + const I = Type.Index(T, ['x']) + Assert.IsFalse(TypeGuard.IsOptional(I)) + Assert.IsTrue(TypeGuard.IsIntersect(I)) + Assert.IsTrue(TypeGuard.IsString(I.allOf[0])) + Assert.IsTrue(TypeGuard.IsString(I.allOf[1])) + }) + it('Normalize Intersect 4', () => { + const T = Type.Intersect([Type.Object({ x: Type.String() })]) + const I = Type.Index(T, ['x']) + Assert.IsTrue(TypeGuard.IsString(I)) + }) + it('Normalize Intersect 5', () => { + const T = Type.Intersect([Type.Object({ x: Type.String() }), Type.Object({ y: Type.String() })]) + const I = Type.Index(T, ['x', 'y']) + Assert.IsFalse(TypeGuard.IsOptional(I)) + Assert.IsTrue(TypeGuard.IsUnion(I)) + Assert.IsTrue(TypeGuard.IsString(I.anyOf[0])) + Assert.IsTrue(TypeGuard.IsString(I.anyOf[1])) + }) + it('Normalize Intersect 6', () => { + const T = Type.Recursive(() => Type.Intersect([Type.Object({ x: Type.String() }), Type.Object({ y: Type.String() })])) + const I = Type.Index(T, ['x', 'y']) + Assert.IsFalse(TypeGuard.IsOptional(I)) + Assert.IsTrue(TypeGuard.IsUnion(I)) + Assert.IsTrue(TypeGuard.IsString(I.anyOf[0])) + Assert.IsTrue(TypeGuard.IsString(I.anyOf[1])) + }) + // --------------------------------------------------------- + // Union + // --------------------------------------------------------- + it('Normalize Union 1', () => { + const T = Type.Union([Type.Object({ x: Type.Optional(Type.String()) }), Type.Object({ x: Type.Optional(Type.String()) })]) + const I = Type.Index(T, ['x']) + Assert.IsTrue(TypeGuard.IsOptional(I)) + Assert.IsTrue(TypeGuard.IsUnion(I)) + Assert.IsTrue(TypeGuard.IsString(I.anyOf[0])) + Assert.IsTrue(TypeGuard.IsString(I.anyOf[1])) + }) + it('Normalize Union 2', () => { + const T = Type.Union([Type.Object({ x: Type.Optional(Type.String()) }), Type.Object({ x: Type.String() })]) + const I = Type.Index(T, ['x']) + Assert.IsTrue(TypeGuard.IsOptional(I)) + Assert.IsTrue(TypeGuard.IsUnion(I)) + Assert.IsTrue(TypeGuard.IsString(I.anyOf[0])) + Assert.IsTrue(TypeGuard.IsString(I.anyOf[1])) + }) + it('Normalize Union 3', () => { + const T = Type.Union([Type.Object({ x: Type.String() }), Type.Object({ x: Type.String() })]) + const I = Type.Index(T, ['x']) + Assert.IsFalse(TypeGuard.IsOptional(I)) + Assert.IsTrue(TypeGuard.IsUnion(I)) + Assert.IsTrue(TypeGuard.IsString(I.anyOf[0])) + Assert.IsTrue(TypeGuard.IsString(I.anyOf[1])) + }) + it('Normalize Union 4', () => { + const T = Type.Union([Type.Object({ x: Type.String() })]) + const I = Type.Index(T, ['x']) + Assert.IsTrue(TypeGuard.IsString(I)) + }) + it('Normalize Union 5', () => { + const T = Type.Union([Type.Object({ x: Type.String() }), Type.Object({ y: Type.String() })]) + // @ts-ignore + const I = Type.Index(T, ['x', 'y']) + Assert.IsFalse(TypeGuard.IsNever(I)) + }) + it('Normalize Union 6', () => { + const T = Type.Recursive(() => Type.Union([Type.Object({ x: Type.String() }), Type.Object({ y: Type.String() })])) + // @ts-ignore + const I = Type.Index(T, ['x', 'y']) + Assert.IsFalse(TypeGuard.IsNever(I)) + }) +}) diff --git a/test/runtime/type/normalize/intersect.ts b/test/runtime/type/normalize/intersect.ts new file mode 100644 index 000000000..cf73898a8 --- /dev/null +++ b/test/runtime/type/normalize/intersect.ts @@ -0,0 +1,21 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('type/normalize/Intersect', () => { + it('Normalize 1', () => { + const T = Type.Intersect([Type.Number(), Type.String()]) + const R = TypeGuard.IsIntersect(T) + Assert.IsTrue(R) + }) + it('Normalize 2', () => { + const T = Type.Intersect([Type.Number()]) + const R = TypeGuard.IsNumber(T) + Assert.IsTrue(R) + }) + it('Normalize 3', () => { + const T = Type.Intersect([]) + const R = TypeGuard.IsNever(T) + Assert.IsTrue(R) + }) +}) diff --git a/test/runtime/type/normalize/record.ts b/test/runtime/type/normalize/record.ts new file mode 100644 index 000000000..ef463f5ce --- /dev/null +++ b/test/runtime/type/normalize/record.ts @@ -0,0 +1,12 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('type/normalize/Record', () => { + it('Normalize 1', () => { + const K = Type.Union([Type.Literal('A'), Type.Literal('B')]) + const T = Type.Record(K, Type.String()) + const R = TypeGuard.IsObject(T) + Assert.IsTrue(R) + }) +}) diff --git a/test/runtime/type/normalize/union.ts b/test/runtime/type/normalize/union.ts new file mode 100644 index 000000000..2ab700fb0 --- /dev/null +++ b/test/runtime/type/normalize/union.ts @@ -0,0 +1,21 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('type/normalize/Union', () => { + it('Normalize 1', () => { + const T = Type.Union([Type.Number(), Type.String()]) + const R = TypeGuard.IsUnion(T) + Assert.IsTrue(R) + }) + it('Normalize 2', () => { + const T = Type.Union([Type.Number()]) + const R = TypeGuard.IsNumber(T) + Assert.IsTrue(R) + }) + it('Normalize 3', () => { + const T = Type.Union([]) + const R = TypeGuard.IsNever(T) + Assert.IsTrue(R) + }) +}) diff --git a/test/runtime/type/options/assign-builder.ts b/test/runtime/type/options/assign-builder.ts new file mode 100644 index 000000000..f24e04fa3 --- /dev/null +++ b/test/runtime/type/options/assign-builder.ts @@ -0,0 +1,266 @@ +import { JavaScriptTypeBuilder } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +const Type = new JavaScriptTypeBuilder() + +// TypeBuilder will proxy calls through to the raw function API +describe('type/options/AssignTypeBuilder', () => { + it('Should assign options for Any', () => { + const T = Type.Any({ foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Array', () => { + const T = Type.Array(Type.String(), { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for AsyncIterator', () => { + const T = Type.AsyncIterator(Type.String(), { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Awaited', () => { + const T = Type.Awaited(Type.String(), { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for BigInt', () => { + const T = Type.BigInt({ foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Boolean', () => { + const T = Type.Boolean({ foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Capitalize', () => { + const T = Type.Capitalize(Type.Literal('hello'), { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Composite', () => { + const T = Type.Composite([Type.Object({ x: Type.Number() }), Type.Object({ y: Type.Number() })], { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Const', () => { + const T = Type.Const(1, { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Constructor', () => { + const T = Type.Constructor([], Type.Any(), { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Date', () => { + const T = Type.Date({ foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Enum', () => { + const T = Type.Enum({}, { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Exclude', () => { + const T = Type.Exclude(Type.Union([Type.Literal(1), Type.Literal(2)]), Type.Literal(2), { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Extends', () => { + const T = Type.Extends(Type.String(), Type.String(), Type.Number(), Type.Null(), { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Extract', () => { + const T = Type.Extract(Type.Union([Type.Literal(1), Type.Literal(2)]), Type.Literal(2), { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Function', () => { + const T = Type.Function([], Type.Any(), { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Integer', () => { + const T = Type.Integer({ foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Intersect 1', () => { + const T = Type.Intersect([], { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Intersect 2', () => { + const T = Type.Intersect([Type.Object({ x: Type.Number() })], { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Intersect 3', () => { + const T = Type.Intersect([Type.Object({ x: Type.Number() }), Type.Object({ y: Type.Number() })], { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Iterator', () => { + const T = Type.Iterator(Type.String(), { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for KeyOf', () => { + const T = Type.KeyOf(Type.Object({ x: Type.Number() }), { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Literal', () => { + const T = Type.Literal(1, { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Lowercase', () => { + const T = Type.Lowercase(Type.Literal('hello'), { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Mapped 1', () => { + const M = Type.Mapped(Type.TemplateLiteral('${1|2|3}'), (K) => K, { foo: 'bar' }) + Assert.IsEqual(M.foo, 'bar') + }) + it('Should assign options for Mapped 2', () => { + const T = Type.Object({ x: Type.Number() }) + const M = Type.Mapped(Type.KeyOf(T), (K) => Type.Index(T, K), { foo: 'bar' }) + Assert.IsEqual(M.foo, 'bar') + }) + it('Should assign options for Never', () => { + const T = Type.Never({ foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Not', () => { + const T = Type.Not(Type.String(), { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Null', () => { + const T = Type.Null({ foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Number', () => { + const T = Type.Number({ foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Object', () => { + const T = Type.Object( + { + x: Type.String(), + }, + { foo: 'bar' }, + ) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Omit', () => { + const T = Type.Omit( + Type.Object({ + x: Type.String(), + y: Type.String(), + }), + ['x'], + { foo: 'bar' }, + ) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Partial', () => { + const T = Type.Partial( + Type.Object({ + x: Type.String(), + }), + { foo: 'bar' }, + ) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Pick', () => { + const T = Type.Pick( + Type.Object({ + x: Type.String(), + y: Type.String(), + }), + ['x'], + { foo: 'bar' }, + ) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Record', () => { + const T = Type.Record(Type.String(), Type.Number(), { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Recursive', () => { + const T = Type.Recursive( + (This) => + Type.Object({ + x: Type.Number(), + y: Type.Array(This), + }), + { foo: 'bar' }, + ) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Ref 1', () => { + const T = Type.Object({ x: Type.Number() }, { $id: 'T' }) + const R = Type.Ref(T, { foo: 'bar' }) + Assert.IsEqual(R.foo, 'bar') + }) + it('Should assign options for Ref 2', () => { + const R = Type.Ref('T', { foo: 'bar' }) + Assert.IsEqual(R.foo, 'bar') + }) + it('Should assign options for RegExp', () => { + const R = Type.RegExp(/xyz/, { foo: 'bar' }) + Assert.IsEqual(R.foo, 'bar') + }) + it('Should assign options for Partial', () => { + const T = Type.Required( + Type.Object({ + x: Type.String(), + }), + { foo: 'bar' }, + ) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for String', () => { + const T = Type.String({ foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Symbol', () => { + const T = Type.Symbol({ foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for TemplateLiteral 1', () => { + const T = Type.TemplateLiteral('hello${1|2|3}', { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for TemplateLiteral 2', () => { + const T = Type.TemplateLiteral([Type.Literal('hello'), Type.Union([Type.Literal('1'), Type.Literal('2'), Type.Literal('3')])], { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Tuple 1', () => { + const T = Type.Tuple([], { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Tuple 2', () => { + const T = Type.Tuple([Type.String(), Type.Number()], { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Uint8Array', () => { + const T = Type.Uint8Array({ foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Uncapitalize', () => { + const T = Type.Uncapitalize(Type.Literal('hello'), { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Undefined', () => { + const T = Type.Undefined({ foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Union 1', () => { + const T = Type.Union([], { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Union 1', () => { + const T = Type.Union([Type.String()], { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Union 1', () => { + const T = Type.Union([Type.String(), Type.Boolean()], { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Unknown', () => { + const T = Type.Unknown({ foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Uppercase', () => { + const T = Type.Uppercase(Type.Literal('hello'), { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Void', () => { + const T = Type.Void({ foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) +}) diff --git a/test/runtime/type/options/assign.ts b/test/runtime/type/options/assign.ts new file mode 100644 index 000000000..274d2f4bb --- /dev/null +++ b/test/runtime/type/options/assign.ts @@ -0,0 +1,263 @@ +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('type/options/Assign', () => { + it('Should assign options for Any', () => { + const T = Type.Any({ foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Array', () => { + const T = Type.Array(Type.String(), { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for AsyncIterator', () => { + const T = Type.AsyncIterator(Type.String(), { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Awaited', () => { + const T = Type.Awaited(Type.String(), { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for BigInt', () => { + const T = Type.BigInt({ foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Boolean', () => { + const T = Type.Boolean({ foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Capitalize', () => { + const T = Type.Capitalize(Type.Literal('hello'), { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Composite', () => { + const T = Type.Composite([Type.Object({ x: Type.Number() }), Type.Object({ y: Type.Number() })], { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Const', () => { + const T = Type.Const(1, { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Constructor', () => { + const T = Type.Constructor([], Type.Any(), { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Date', () => { + const T = Type.Date({ foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Enum', () => { + const T = Type.Enum({}, { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Exclude', () => { + const T = Type.Exclude(Type.Union([Type.Literal(1), Type.Literal(2)]), Type.Literal(2), { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Extends', () => { + const T = Type.Extends(Type.String(), Type.String(), Type.Number(), Type.Null(), { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Extract', () => { + const T = Type.Extract(Type.Union([Type.Literal(1), Type.Literal(2)]), Type.Literal(2), { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Function', () => { + const T = Type.Function([], Type.Any(), { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Integer', () => { + const T = Type.Integer({ foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Intersect 1', () => { + const T = Type.Intersect([], { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Intersect 2', () => { + const T = Type.Intersect([Type.Object({ x: Type.Number() })], { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Intersect 3', () => { + const T = Type.Intersect([Type.Object({ x: Type.Number() }), Type.Object({ y: Type.Number() })], { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Iterator', () => { + const T = Type.Iterator(Type.String(), { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for KeyOf', () => { + const T = Type.KeyOf(Type.Object({ x: Type.Number() }), { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Literal', () => { + const T = Type.Literal(1, { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Lowercase', () => { + const T = Type.Lowercase(Type.Literal('hello'), { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Mapped 1', () => { + const M = Type.Mapped(Type.TemplateLiteral('${1|2|3}'), (K) => K, { foo: 'bar' }) + Assert.IsEqual(M.foo, 'bar') + }) + it('Should assign options for Mapped 2', () => { + const T = Type.Object({ x: Type.Number() }) + const M = Type.Mapped(Type.KeyOf(T), (K) => Type.Index(T, K), { foo: 'bar' }) + Assert.IsEqual(M.foo, 'bar') + }) + it('Should assign options for Never', () => { + const T = Type.Never({ foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Not', () => { + const T = Type.Not(Type.String(), { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Null', () => { + const T = Type.Null({ foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Number', () => { + const T = Type.Number({ foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Object', () => { + const T = Type.Object( + { + x: Type.String(), + }, + { foo: 'bar' }, + ) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Omit', () => { + const T = Type.Omit( + Type.Object({ + x: Type.String(), + y: Type.String(), + }), + ['x'], + { foo: 'bar' }, + ) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Partial', () => { + const T = Type.Partial( + Type.Object({ + x: Type.String(), + }), + { foo: 'bar' }, + ) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Pick', () => { + const T = Type.Pick( + Type.Object({ + x: Type.String(), + y: Type.String(), + }), + ['x'], + { foo: 'bar' }, + ) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Record', () => { + const T = Type.Record(Type.String(), Type.Number(), { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Recursive', () => { + const T = Type.Recursive( + (This) => + Type.Object({ + x: Type.Number(), + y: Type.Array(This), + }), + { foo: 'bar' }, + ) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Ref 1', () => { + const T = Type.Object({ x: Type.Number() }, { $id: 'T' }) + const R = Type.Ref(T, { foo: 'bar' }) + Assert.IsEqual(R.foo, 'bar') + }) + it('Should assign options for Ref 2', () => { + const R = Type.Ref('T', { foo: 'bar' }) + Assert.IsEqual(R.foo, 'bar') + }) + it('Should assign options for RegExp', () => { + const R = Type.RegExp(/xyz/, { foo: 'bar' }) + Assert.IsEqual(R.foo, 'bar') + }) + it('Should assign options for Partial', () => { + const T = Type.Required( + Type.Object({ + x: Type.String(), + }), + { foo: 'bar' }, + ) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for String', () => { + const T = Type.String({ foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Symbol', () => { + const T = Type.Symbol({ foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for TemplateLiteral 1', () => { + const T = Type.TemplateLiteral('hello${1|2|3}', { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for TemplateLiteral 2', () => { + const T = Type.TemplateLiteral([Type.Literal('hello'), Type.Union([Type.Literal('1'), Type.Literal('2'), Type.Literal('3')])], { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Tuple 1', () => { + const T = Type.Tuple([], { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Tuple 2', () => { + const T = Type.Tuple([Type.String(), Type.Number()], { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Uint8Array', () => { + const T = Type.Uint8Array({ foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Uncapitalize', () => { + const T = Type.Uncapitalize(Type.Literal('hello'), { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Undefined', () => { + const T = Type.Undefined({ foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Union 1', () => { + const T = Type.Union([], { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Union 1', () => { + const T = Type.Union([Type.String()], { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Union 1', () => { + const T = Type.Union([Type.String(), Type.Boolean()], { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Unknown', () => { + const T = Type.Unknown({ foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Uppercase', () => { + const T = Type.Uppercase(Type.Literal('hello'), { foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) + it('Should assign options for Void', () => { + const T = Type.Void({ foo: 'bar' }) + Assert.IsEqual(T.foo, 'bar') + }) +}) diff --git a/test/runtime/type/options/index.ts b/test/runtime/type/options/index.ts new file mode 100644 index 000000000..2b378f7fd --- /dev/null +++ b/test/runtime/type/options/index.ts @@ -0,0 +1,2 @@ +import './assign-builder' +import './assign' diff --git a/test/runtime/type/registry/format.ts b/test/runtime/type/registry/format.ts new file mode 100644 index 000000000..812a3de6f --- /dev/null +++ b/test/runtime/type/registry/format.ts @@ -0,0 +1,22 @@ +import { FormatRegistry } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('type/registry/Format', () => { + it('Should set format', () => { + FormatRegistry.Set('test#format1', () => true) + }) + it('Should get format', () => { + FormatRegistry.Set('test#format2', () => true) + const format = FormatRegistry.Get('test#format2') + Assert.IsEqual(typeof format, 'function') + }) + it('Should return true if exists', () => { + FormatRegistry.Set('test#format3', () => true) + Assert.IsTrue(FormatRegistry.Has('test#format3')) + }) + it('Should clear formats', () => { + FormatRegistry.Set('test#format4', () => true) + FormatRegistry.Clear() + Assert.IsFalse(FormatRegistry.Has('test#format4')) + }) +}) diff --git a/test/runtime/type/registry/index.ts b/test/runtime/type/registry/index.ts new file mode 100644 index 000000000..1afccae94 --- /dev/null +++ b/test/runtime/type/registry/index.ts @@ -0,0 +1,2 @@ +import './format' +import './type' diff --git a/test/runtime/type/registry/type.ts b/test/runtime/type/registry/type.ts new file mode 100644 index 000000000..b36d2a8d4 --- /dev/null +++ b/test/runtime/type/registry/type.ts @@ -0,0 +1,22 @@ +import { TypeRegistry } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('type/registry/Type', () => { + it('Should set type', () => { + TypeRegistry.Set('test#type1', () => true) + }) + it('Should get type', () => { + TypeRegistry.Set('test#type2', () => true) + const format = TypeRegistry.Get('test#type2') + Assert.IsEqual(typeof format, 'function') + }) + it('Should return true if exists', () => { + TypeRegistry.Set('test#type3', () => true) + Assert.IsTrue(TypeRegistry.Has('test#type3')) + }) + it('Should clear types', () => { + TypeRegistry.Set('test#type4', () => true) + TypeRegistry.Clear() + Assert.IsFalse(TypeRegistry.Has('test#type4')) + }) +}) diff --git a/test/runtime/type/sets/index.ts b/test/runtime/type/sets/index.ts new file mode 100644 index 000000000..2b3525728 --- /dev/null +++ b/test/runtime/type/sets/index.ts @@ -0,0 +1 @@ +import './sets' diff --git a/test/runtime/type/sets/sets.ts b/test/runtime/type/sets/sets.ts new file mode 100644 index 000000000..6bfef3c09 --- /dev/null +++ b/test/runtime/type/sets/sets.ts @@ -0,0 +1,167 @@ +import * as Type from '@sinclair/typebox' +import { Assert } from '../../assert' + +describe('type/sets', () => { + // ---------------------------------------------------------------- + // Distinct + // ---------------------------------------------------------------- + it('Should Distinct', () => { + const R = Type.SetDistinct([1, 1, 2, 2]) + Assert.IsEqual(R, [1, 2]) + }) + // ---------------------------------------------------------------- + // Includes + // ---------------------------------------------------------------- + it('Should Includes 1', () => { + const R = Type.SetIncludes([1, 2, 3, 4], 1) + Assert.IsTrue(R) + }) + it('Should Includes 2', () => { + const R = Type.SetIncludes([1, 2, 3, 4], 7) + Assert.IsFalse(R) + }) + // ---------------------------------------------------------------- + // IsSubset + // ---------------------------------------------------------------- + it('Should IsSubset 1', () => { + const R = Type.SetIsSubset([1, 2], [1, 2]) + Assert.IsTrue(R) + }) + it('Should IsSubset 2', () => { + const R = Type.SetIsSubset([1, 2], [1, 2, 3]) + Assert.IsTrue(R) + }) + it('Should IsSubset 3', () => { + const R = Type.SetIsSubset([1, 2], [1]) + Assert.IsFalse(R) + }) + // ---------------------------------------------------------------- + // Intersect + // ---------------------------------------------------------------- + it('Should Intersect 1', () => { + const R = Type.SetIntersect([1, 2], [1, 2]) + Assert.IsEqual(R, [1, 2]) + }) + it('Should Intersect 2', () => { + const R = Type.SetIntersect([1], [1, 2]) + Assert.IsEqual(R, [1]) + }) + it('Should Intersect 3', () => { + const R = Type.SetIntersect([1, 2], [1]) + Assert.IsEqual(R, [1]) + }) + it('Should Intersect 4', () => { + const R = Type.SetIntersect([], [1]) + Assert.IsEqual(R, []) + }) + it('Should Intersect 5', () => { + const R = Type.SetIntersect([1], []) + Assert.IsEqual(R, []) + }) + it('Should Intersect 6', () => { + const R = Type.SetIntersect([1], [2]) + Assert.IsEqual(R, []) + }) + // ---------------------------------------------------------------- + // Union + // ---------------------------------------------------------------- + it('Should Union 1', () => { + const R = Type.SetUnion([1, 2], [1, 2]) + Assert.IsEqual(R, [1, 2, 1, 2]) + }) + it('Should Union 2', () => { + const R = Type.SetUnion([1], [1, 2]) + Assert.IsEqual(R, [1, 1, 2]) + }) + it('Should Union 3', () => { + const R = Type.SetUnion([1, 2], [1]) + Assert.IsEqual(R, [1, 2, 1]) + }) + it('Should Union 4', () => { + const R = Type.SetUnion([], [1]) + Assert.IsEqual(R, [1]) + }) + it('Should Union 5', () => { + const R = Type.SetUnion([1], []) + Assert.IsEqual(R, [1]) + }) + // ---------------------------------------------------------------- + // Complement + // ---------------------------------------------------------------- + it('Should Complement 1', () => { + const R = Type.SetComplement([1, 2, 3, 4], [2, 3]) + Assert.IsEqual(R, [1, 4]) + }) + it('Should Complement 2', () => { + const R = Type.SetComplement([2, 3], [1, 2, 3, 4]) + Assert.IsEqual(R, []) + }) + // ---------------------------------------------------------------- + // IntersectMany + // ---------------------------------------------------------------- + it('Should IntersectMany 1', () => { + const R = Type.SetIntersectMany([[1, 2, 3], [1, 2], [1]] as const) + Assert.IsEqual(R, [1]) + }) + it('Should IntersectMany 2', () => { + const R = Type.SetIntersectMany([[1], [1, 2], [1, 2, 3]] as const) + Assert.IsEqual(R, [1]) + }) + it('Should IntersectMany 3', () => { + const R = Type.SetIntersectMany([ + [1, 2], + [1, 2], + ] as const) + Assert.IsEqual(R, [1, 2]) + }) + it('Should IntersectMany 4', () => { + const R = Type.SetIntersectMany([[1], [2]] as const) + Assert.IsEqual(R, []) + }) + it('Should IntersectMany 5', () => { + const R = Type.SetIntersectMany([[1], []] as const) + Assert.IsEqual(R, []) + }) + it('Should IntersectMany 6', () => { + const R = Type.SetIntersectMany([[], [1]] as const) + Assert.IsEqual(R, []) + }) + it('Should IntersectMany 7', () => { + const R = Type.SetIntersectMany([[1], [1], [1], [1], []] as const) + Assert.IsEqual(R, []) + }) + it('Should IntersectMany 8', () => { + const R = Type.SetIntersectMany([[], [1], [1], [1], [1]] as const) + Assert.IsEqual(R, []) + }) + // ---------------------------------------------------------------- + // UnionMany + // ---------------------------------------------------------------- + it('Should UnionMany 1', () => { + const R = Type.SetUnionMany([[1, 2, 3], [1, 2], [1]] as const) + Assert.IsEqual(R, [1, 2, 3, 1, 2, 1]) + }) + it('Should UnionMany 2', () => { + const R = Type.SetUnionMany([[1], [1, 2], [1, 2, 3]] as const) + Assert.IsEqual(R, [1, 1, 2, 1, 2, 3]) + }) + it('Should UnionMany 3', () => { + const R = Type.SetUnionMany([ + [1, 2], + [1, 2], + ] as const) + Assert.IsEqual(R, [1, 2, 1, 2]) + }) + it('Should UnionMany 4', () => { + const R = Type.SetUnionMany([[1], [2]] as const) + Assert.IsEqual(R, [1, 2]) + }) + it('Should UnionMany 5', () => { + const R = Type.SetUnionMany([[1], []] as const) + Assert.IsEqual(R, [1]) + }) + it('Should UnionMany 6', () => { + const R = Type.SetUnionMany([[], [1]] as const) + Assert.IsEqual(R, [1]) + }) +}) diff --git a/test/runtime/type/template-literal/finite.ts b/test/runtime/type/template-literal/finite.ts new file mode 100644 index 000000000..a6a97efa3 --- /dev/null +++ b/test/runtime/type/template-literal/finite.ts @@ -0,0 +1,71 @@ +import { TemplateLiteralParse, IsTemplateLiteralExpressionFinite, PatternString, PatternBoolean, PatternNumber } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('type/template-literal/IsTemplateLiteralExpressionFinite', () => { + // --------------------------------------------------------------- + // Finite + // --------------------------------------------------------------- + it('Finite 1', () => { + const E = TemplateLiteralParse(`A`) + const R = IsTemplateLiteralExpressionFinite(E) + Assert.IsTrue(R) + }) + it('Finite 2', () => { + const E = TemplateLiteralParse(`A|B`) + const R = IsTemplateLiteralExpressionFinite(E) + Assert.IsTrue(R) + }) + it('Finite 3', () => { + const E = TemplateLiteralParse(`A(B|C)`) + const R = IsTemplateLiteralExpressionFinite(E) + Assert.IsTrue(R) + }) + it('Finite 4', () => { + const E = TemplateLiteralParse(`${PatternBoolean}`) + const R = IsTemplateLiteralExpressionFinite(E) + Assert.IsTrue(R) + }) + it('Finite 5', () => { + const E = TemplateLiteralParse(`\\.\\*`) + const R = IsTemplateLiteralExpressionFinite(E) + Assert.IsTrue(R) + }) + // --------------------------------------------------------------- + // Infinite + // --------------------------------------------------------------- + it('Infinite 1', () => { + const E = TemplateLiteralParse(`${PatternString}`) + const R = IsTemplateLiteralExpressionFinite(E) + Assert.IsFalse(R) + }) + it('Infinite 2', () => { + const E = TemplateLiteralParse(`${PatternNumber}`) + const R = IsTemplateLiteralExpressionFinite(E) + Assert.IsFalse(R) + }) + it('Infinite 3', () => { + const E = TemplateLiteralParse(`A|${PatternString}`) + const R = IsTemplateLiteralExpressionFinite(E) + Assert.IsFalse(R) + }) + it('Infinite 4', () => { + const E = TemplateLiteralParse(`A|${PatternNumber}`) + const R = IsTemplateLiteralExpressionFinite(E) + Assert.IsFalse(R) + }) + it('Infinite 5', () => { + const E = TemplateLiteralParse(`A(${PatternString})`) + const R = IsTemplateLiteralExpressionFinite(E) + Assert.IsFalse(R) + }) + it('Infinite 6', () => { + const E = TemplateLiteralParse(`A(${PatternNumber})`) + const R = IsTemplateLiteralExpressionFinite(E) + Assert.IsFalse(R) + }) + it('Infinite 7', () => { + const E = TemplateLiteralParse(`${PatternString}_foo`) + const R = IsTemplateLiteralExpressionFinite(E) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/type/template-literal/generate.ts b/test/runtime/type/template-literal/generate.ts new file mode 100644 index 000000000..197754750 --- /dev/null +++ b/test/runtime/type/template-literal/generate.ts @@ -0,0 +1,204 @@ +import { TemplateLiteralParse, TemplateLiteralExpressionGenerate } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('type/template-literal/TemplateLiteralExpressionGenerate', () => { + // --------------------------------------------------------------- + // Exact (No Default Unwrap) + // --------------------------------------------------------------- + it('Exact 1', () => { + const E = TemplateLiteralParse('^$') + const R = [...TemplateLiteralExpressionGenerate(E)] + Assert.IsEqual(R, ['^$']) + }) + it('Exact 2', () => { + const E = TemplateLiteralParse('^A$') + const R = [...TemplateLiteralExpressionGenerate(E)] + Assert.IsEqual(R, ['^A$']) + }) + // --------------------------------------------------------------- + // Patterns + // --------------------------------------------------------------- + it('Pattern 1', () => { + const E = TemplateLiteralParse('(true|false)') + const R = [...TemplateLiteralExpressionGenerate(E)] + Assert.IsEqual(R, ['true', 'false']) + }) + it('Pattern 2', () => { + const E = TemplateLiteralParse('(0|[1-9][0-9]*)') + const R = [...TemplateLiteralExpressionGenerate(E)] + Assert.IsEqual(R, ['0', '[1-9][0-9]*']) + }) + it('Pattern 3', () => { + const E = TemplateLiteralParse('.*') + const R = [...TemplateLiteralExpressionGenerate(E)] + Assert.IsEqual(R, ['.*']) + }) + // --------------------------------------------------------------- + // Expression + // --------------------------------------------------------------- + it('Expression 1', () => { + const E = TemplateLiteralParse(')') + const R = [...TemplateLiteralExpressionGenerate(E)] + Assert.IsEqual(R, [')']) + }) + it('Expression 2', () => { + const E = TemplateLiteralParse('\\)') + const R = [...TemplateLiteralExpressionGenerate(E)] + Assert.IsEqual(R, [')']) + }) + it('Expression 3', () => { + const E = TemplateLiteralParse('\\(') + const R = [...TemplateLiteralExpressionGenerate(E)] + Assert.IsEqual(R, ['(']) + }) + it('Expression 4', () => { + const E = TemplateLiteralParse('') + const R = [...TemplateLiteralExpressionGenerate(E)] + Assert.IsEqual(R, ['']) + }) + it('Expression 5', () => { + const E = TemplateLiteralParse('\\') + const R = [...TemplateLiteralExpressionGenerate(E)] + Assert.IsEqual(R, ['\\']) + }) + it('Expression 6', () => { + const E = TemplateLiteralParse('()') + const R = [...TemplateLiteralExpressionGenerate(E)] + Assert.IsEqual(R, ['']) + }) + it('Expression 7', () => { + const E = TemplateLiteralParse('(a)') + const R = [...TemplateLiteralExpressionGenerate(E)] + Assert.IsEqual(R, ['a']) + }) + it('Expression 8', () => { + const E = TemplateLiteralParse('()))') + const R = [...TemplateLiteralExpressionGenerate(E)] + Assert.IsEqual(R, ['))']) + }) + it('Expression 9', () => { + const E = TemplateLiteralParse('())') + const R = [...TemplateLiteralExpressionGenerate(E)] + Assert.IsEqual(R, [')']) + }) + it('Expression 10', () => { + const E = TemplateLiteralParse('A|B') + const R = [...TemplateLiteralExpressionGenerate(E)] + Assert.IsEqual(R, ['A', 'B']) + }) + it('Expression 11', () => { + const E = TemplateLiteralParse('A|(B)') + const R = [...TemplateLiteralExpressionGenerate(E)] + Assert.IsEqual(R, ['A', 'B']) + }) + it('Expression 12', () => { + const E = TemplateLiteralParse('A(B)') + const R = [...TemplateLiteralExpressionGenerate(E)] + Assert.IsEqual(R, ['AB']) + }) + it('Expression 13', () => { + const E = TemplateLiteralParse('(A)B') + const R = [...TemplateLiteralExpressionGenerate(E)] + Assert.IsEqual(R, ['AB']) + }) + it('Expression 14', () => { + const E = TemplateLiteralParse('(A)|B') + const R = [...TemplateLiteralExpressionGenerate(E)] + Assert.IsEqual(R, ['A', 'B']) + }) + it('Expression 15', () => { + const E = TemplateLiteralParse('|') + const R = [...TemplateLiteralExpressionGenerate(E)] + Assert.IsEqual(R, ['']) + }) + it('Expression 16', () => { + const E = TemplateLiteralParse('||') + const R = [...TemplateLiteralExpressionGenerate(E)] + Assert.IsEqual(R, ['']) + }) + it('Expression 17', () => { + const E = TemplateLiteralParse('||A') + const R = [...TemplateLiteralExpressionGenerate(E)] + Assert.IsEqual(R, ['A']) + }) + it('Expression 18', () => { + const E = TemplateLiteralParse('A||') + const R = [...TemplateLiteralExpressionGenerate(E)] + Assert.IsEqual(R, ['A']) + }) + it('Expression 19', () => { + const E = TemplateLiteralParse('A||B') + const R = [...TemplateLiteralExpressionGenerate(E)] + Assert.IsEqual(R, ['A', 'B']) + }) + it('Expression 20', () => { + const E = TemplateLiteralParse('A|()|B') + const R = [...TemplateLiteralExpressionGenerate(E)] + Assert.IsEqual(R, ['A', '', 'B']) + }) + it('Expression 21', () => { + const E = TemplateLiteralParse('A|(|)|B') + const R = [...TemplateLiteralExpressionGenerate(E)] + Assert.IsEqual(R, ['A', '', 'B']) + }) + it('Expression 22', () => { + const E = TemplateLiteralParse('A|(||)|B') + const R = [...TemplateLiteralExpressionGenerate(E)] + Assert.IsEqual(R, ['A', '', 'B']) + }) + it('Expression 23', () => { + const E = TemplateLiteralParse('|A(||)B|') + const R = [...TemplateLiteralExpressionGenerate(E)] + Assert.IsEqual(R, ['AB']) + }) + it('Expression 24', () => { + const E = TemplateLiteralParse('A(B)(C)') + const R = [...TemplateLiteralExpressionGenerate(E)] + Assert.IsEqual(R, ['ABC']) + }) + it('Expression 25', () => { + const E = TemplateLiteralParse('A(B)|(C)') + const R = [...TemplateLiteralExpressionGenerate(E)] + Assert.IsEqual(R, ['AB', 'C']) + }) + it('Expression 26', () => { + const E = TemplateLiteralParse('A(B|C)') + const R = [...TemplateLiteralExpressionGenerate(E)] + Assert.IsEqual(R, ['AB', 'AC']) + }) + it('Expression 27', () => { + const E = TemplateLiteralParse('A|(B|C)') + const R = [...TemplateLiteralExpressionGenerate(E)] + Assert.IsEqual(R, ['A', 'B', 'C']) + }) + it('Expression 28', () => { + const E = TemplateLiteralParse('((A)B)C') + const R = [...TemplateLiteralExpressionGenerate(E)] + Assert.IsEqual(R, ['ABC']) + }) + it('Expression 29', () => { + const E = TemplateLiteralParse('(0|1)(0|1)') + const R = [...TemplateLiteralExpressionGenerate(E)] + Assert.IsEqual(R, ['00', '01', '10', '11']) + }) + it('Expression 30', () => { + const E = TemplateLiteralParse('(0|1)|(0|1)') + const R = [...TemplateLiteralExpressionGenerate(E)] + Assert.IsEqual(R, ['0', '1', '0', '1']) + }) + it('Expression 31', () => { + const E = TemplateLiteralParse('(0|(1|0)|1)') + const R = [...TemplateLiteralExpressionGenerate(E)] + Assert.IsEqual(R, ['0', '1', '0', '1']) + }) + it('Expression 32', () => { + const E = TemplateLiteralParse('(0(1|0)1)') + const R = [...TemplateLiteralExpressionGenerate(E)] + Assert.IsEqual(R, ['011', '001']) + }) + it('Expression 33', () => { + const E = TemplateLiteralParse('\\$prop(1|2|3)') + const R = [...TemplateLiteralExpressionGenerate(E)] + Assert.IsEqual(R, ['$prop1', '$prop2', '$prop3']) + }) +}) diff --git a/test/runtime/type/template-literal/index.ts b/test/runtime/type/template-literal/index.ts new file mode 100644 index 000000000..483e67965 --- /dev/null +++ b/test/runtime/type/template-literal/index.ts @@ -0,0 +1,4 @@ +import './finite' +import './generate' +import './parse' +import './pattern' diff --git a/test/runtime/type/template-literal/parse.ts b/test/runtime/type/template-literal/parse.ts new file mode 100644 index 000000000..679cb4b21 --- /dev/null +++ b/test/runtime/type/template-literal/parse.ts @@ -0,0 +1,607 @@ +import { TemplateLiteralParse } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('type/template-literal/TemplateLiteralParser', () => { + // --------------------------------------------------------------- + // Throws + // --------------------------------------------------------------- + it('Throw 1', () => { + Assert.Throws(() => TemplateLiteralParse('(')) + }) + it('Throw 2', () => { + Assert.Throws(() => TemplateLiteralParse('(')) + }) + // --------------------------------------------------------------- + // Exact (No Default Unwrap) + // --------------------------------------------------------------- + it('Exact 1', () => { + const E = TemplateLiteralParse('^$') + Assert.IsEqual(E, { + type: 'const', + const: '^$', + }) + }) + it('Exact 2', () => { + const E = TemplateLiteralParse('^A$') + Assert.IsEqual(E, { + type: 'const', + const: '^A$', + }) + }) + // --------------------------------------------------------------- + // Patterns + // --------------------------------------------------------------- + it('Pattern 1', () => { + const E = TemplateLiteralParse('(true|false)') + Assert.IsEqual(E, { + type: 'or', + expr: [ + { + type: 'const', + const: 'true', + }, + { + type: 'const', + const: 'false', + }, + ], + }) + }) + it('Pattern 2', () => { + const E = TemplateLiteralParse('(0|[1-9][0-9]*)') + Assert.IsEqual(E, { + type: 'or', + expr: [ + { + type: 'const', + const: '0', + }, + { + type: 'const', + const: '[1-9][0-9]*', + }, + ], + }) + }) + it('Pattern 3', () => { + const E = TemplateLiteralParse('.*') + Assert.IsEqual(E, { + type: 'const', + const: '.*', + }) + }) + // --------------------------------------------------------------- + // Expression + // --------------------------------------------------------------- + it('Expression 1', () => { + const E = TemplateLiteralParse(')') + Assert.IsEqual(E, { + type: 'const', + const: ')', + }) + }) + it('Expression 2', () => { + const E = TemplateLiteralParse('\\)') + Assert.IsEqual(E, { + type: 'const', + const: ')', + }) + }) + it('Expression 3', () => { + const E = TemplateLiteralParse('\\(') + Assert.IsEqual(E, { + type: 'const', + const: '(', + }) + }) + it('Expression 4', () => { + const E = TemplateLiteralParse('') + Assert.IsEqual(E, { + type: 'const', + const: '', + }) + }) + it('Expression 5', () => { + const E = TemplateLiteralParse('\\') + Assert.IsEqual(E, { + type: 'const', + const: '\\', + }) + }) + it('Expression 6', () => { + const E = TemplateLiteralParse('()') + Assert.IsEqual(E, { + type: 'const', + const: '', + }) + }) + it('Expression 7', () => { + const E = TemplateLiteralParse('(a)') + Assert.IsEqual(E, { + type: 'const', + const: 'a', + }) + }) + it('Expression 8', () => { + const E = TemplateLiteralParse('()))') + Assert.IsEqual(E, { + type: 'and', + expr: [ + { + type: 'const', + const: '', + }, + { + type: 'const', + const: '))', + }, + ], + }) + }) + it('Expression 9', () => { + const E = TemplateLiteralParse('())') + Assert.IsEqual(E, { + type: 'and', + expr: [ + { + type: 'const', + const: '', + }, + { + type: 'const', + const: ')', + }, + ], + }) + }) + it('Expression 10', () => { + const E = TemplateLiteralParse('A|B') + Assert.IsEqual(E, { + type: 'or', + expr: [ + { + type: 'const', + const: 'A', + }, + { + type: 'const', + const: 'B', + }, + ], + }) + }) + it('Expression 11', () => { + const E = TemplateLiteralParse('A|(B)') + Assert.IsEqual(E, { + type: 'or', + expr: [ + { + type: 'const', + const: 'A', + }, + { + type: 'const', + const: 'B', + }, + ], + }) + }) + it('Expression 12', () => { + const E = TemplateLiteralParse('A(B)') + Assert.IsEqual(E, { + type: 'and', + expr: [ + { + type: 'const', + const: 'A', + }, + { + type: 'const', + const: 'B', + }, + ], + }) + }) + it('Expression 13', () => { + const E = TemplateLiteralParse('(A)B') + Assert.IsEqual(E, { + type: 'and', + expr: [ + { + type: 'const', + const: 'A', + }, + { + type: 'const', + const: 'B', + }, + ], + }) + }) + it('Expression 14', () => { + const E = TemplateLiteralParse('(A)|B') + Assert.IsEqual(E, { + type: 'or', + expr: [ + { + type: 'const', + const: 'A', + }, + { + type: 'const', + const: 'B', + }, + ], + }) + }) + it('Expression 15', () => { + const E = TemplateLiteralParse('|') + Assert.IsEqual(E, { + type: 'const', + const: '', + }) + }) + it('Expression 16', () => { + const E = TemplateLiteralParse('||') + Assert.IsEqual(E, { + type: 'const', + const: '', + }) + }) + it('Expression 17', () => { + const E = TemplateLiteralParse('||A') + Assert.IsEqual(E, { + type: 'const', + const: 'A', + }) + }) + it('Expression 18', () => { + const E = TemplateLiteralParse('A||') + Assert.IsEqual(E, { + type: 'const', + const: 'A', + }) + }) + it('Expression 19', () => { + const E = TemplateLiteralParse('A||B') + Assert.IsEqual(E, { + type: 'or', + expr: [ + { + type: 'const', + const: 'A', + }, + { + type: 'const', + const: 'B', + }, + ], + }) + }) + it('Expression 20', () => { + const E = TemplateLiteralParse('A|()|B') + Assert.IsEqual(E, { + type: 'or', + expr: [ + { + type: 'const', + const: 'A', + }, + { + type: 'const', + const: '', + }, + { + type: 'const', + const: 'B', + }, + ], + }) + }) + it('Expression 21', () => { + const E = TemplateLiteralParse('A|(|)|B') + Assert.IsEqual(E, { + type: 'or', + expr: [ + { + type: 'const', + const: 'A', + }, + { + type: 'const', + const: '', + }, + { + type: 'const', + const: 'B', + }, + ], + }) + }) + it('Expression 22', () => { + const E = TemplateLiteralParse('A|(||)|B') + Assert.IsEqual(E, { + type: 'or', + expr: [ + { + type: 'const', + const: 'A', + }, + { + type: 'const', + const: '', + }, + { + type: 'const', + const: 'B', + }, + ], + }) + }) + it('Expression 23', () => { + const E = TemplateLiteralParse('|A(||)B|') + Assert.IsEqual(E, { + type: 'and', + expr: [ + { + type: 'const', + const: 'A', + }, + { + type: 'const', + const: '', + }, + { + type: 'const', + const: 'B', + }, + ], + }) + }) + it('Expression 24', () => { + const E = TemplateLiteralParse('A(B)(C)') + Assert.IsEqual(E, { + type: 'and', + expr: [ + { + type: 'const', + const: 'A', + }, + { + type: 'const', + const: 'B', + }, + { + type: 'const', + const: 'C', + }, + ], + }) + }) + it('Expression 25', () => { + const E = TemplateLiteralParse('A(B)|(C)') + Assert.IsEqual(E, { + type: 'or', + expr: [ + { + type: 'and', + expr: [ + { + type: 'const', + const: 'A', + }, + { + type: 'const', + const: 'B', + }, + ], + }, + { + type: 'const', + const: 'C', + }, + ], + }) + }) + it('Expression 26', () => { + const E = TemplateLiteralParse('A(B|C)') + Assert.IsEqual(E, { + type: 'and', + expr: [ + { + type: 'const', + const: 'A', + }, + { + type: 'or', + expr: [ + { + type: 'const', + const: 'B', + }, + { + type: 'const', + const: 'C', + }, + ], + }, + ], + }) + }) + it('Expression 27', () => { + const E = TemplateLiteralParse('A|(B|C)') + Assert.IsEqual(E, { + type: 'or', + expr: [ + { + type: 'const', + const: 'A', + }, + { + type: 'or', + expr: [ + { + type: 'const', + const: 'B', + }, + { + type: 'const', + const: 'C', + }, + ], + }, + ], + }) + }) + it('Expression 28', () => { + const E = TemplateLiteralParse('((A)B)C') + Assert.IsEqual(E, { + type: 'and', + expr: [ + { + type: 'and', + expr: [ + { + type: 'const', + const: 'A', + }, + { + type: 'const', + const: 'B', + }, + ], + }, + { + type: 'const', + const: 'C', + }, + ], + }) + }) + it('Expression 29', () => { + const E = TemplateLiteralParse('(0|1)(0|1)') + Assert.IsEqual(E, { + type: 'and', + expr: [ + { + type: 'or', + expr: [ + { + type: 'const', + const: '0', + }, + { + type: 'const', + const: '1', + }, + ], + }, + { + type: 'or', + expr: [ + { + type: 'const', + const: '0', + }, + { + type: 'const', + const: '1', + }, + ], + }, + ], + }) + }) + it('Expression 30', () => { + const E = TemplateLiteralParse('(0|1)|(0|1)') + Assert.IsEqual(E, { + type: 'or', + expr: [ + { + type: 'or', + expr: [ + { + type: 'const', + const: '0', + }, + { + type: 'const', + const: '1', + }, + ], + }, + { + type: 'or', + expr: [ + { + type: 'const', + const: '0', + }, + { + type: 'const', + const: '1', + }, + ], + }, + ], + }) + }) + it('Expression 31', () => { + const E = TemplateLiteralParse('(0|(1|0)|1)') + Assert.IsEqual(E, { + type: 'or', + expr: [ + { + type: 'const', + const: '0', + }, + { + type: 'or', + expr: [ + { + type: 'const', + const: '1', + }, + { + type: 'const', + const: '0', + }, + ], + }, + { + type: 'const', + const: '1', + }, + ], + }) + }) + it('Expression 32', () => { + const E = TemplateLiteralParse('(0(1|0)1)') + Assert.IsEqual(E, { + type: 'and', + expr: [ + { + type: 'const', + const: '0', + }, + { + type: 'or', + expr: [ + { + type: 'const', + const: '1', + }, + { + type: 'const', + const: '0', + }, + ], + }, + { + type: 'const', + const: '1', + }, + ], + }) + }) +}) diff --git a/test/runtime/type/template-literal/pattern.ts b/test/runtime/type/template-literal/pattern.ts new file mode 100644 index 000000000..1a8b13fa8 --- /dev/null +++ b/test/runtime/type/template-literal/pattern.ts @@ -0,0 +1,164 @@ +import { Type, TTemplateLiteral, PatternNumber, PatternString, PatternBoolean } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('type/template-literal/TemplateLiteralPattern', () => { + const Equal = (template: TTemplateLiteral, expect: string) => { + const pattern = template.pattern.slice(1, template.pattern.length - 1) + Assert.IsEqual(pattern, expect) + } + // --------------------------------------------------------------- + // Escape + // --------------------------------------------------------------- + it('Escape 1', () => { + const T = Type.TemplateLiteral([Type.Literal('.*')]) + Assert.IsEqual(T.pattern, '^\\.\\*$') + }) + it('Escape 2', () => { + const T = Type.TemplateLiteral([Type.Literal('(')]) + Assert.IsEqual(T.pattern, '^\\($') + }) + it('Escape 3', () => { + const T = Type.TemplateLiteral([Type.Literal(')')]) + Assert.IsEqual(T.pattern, '^\\)$') + }) + it('Escape 4', () => { + const T = Type.TemplateLiteral([Type.Literal('|')]) + Assert.IsEqual(T.pattern, '^\\|$') + }) + // --------------------------------------------------------------- + // Pattern + // --------------------------------------------------------------- + it('Pattern 1', () => { + const T = Type.TemplateLiteral([Type.Number()]) + Equal(T, `${PatternNumber}`) + }) + it('Pattern 2', () => { + const T = Type.TemplateLiteral([Type.String()]) + Equal(T, `${PatternString}`) + }) + it('Pattern 3', () => { + const T = Type.TemplateLiteral([Type.Boolean()]) + Equal(T, `${PatternBoolean}`) + }) + it('Pattern 4', () => { + const T = Type.TemplateLiteral([Type.Literal('A'), Type.Number()]) + Equal(T, `A${PatternNumber}`) + }) + it('Pattern 5', () => { + const T = Type.TemplateLiteral([Type.Literal('A'), Type.String()]) + Equal(T, `A${PatternString}`) + }) + it('Pattern 6', () => { + const T = Type.TemplateLiteral([Type.Literal('A'), Type.Boolean()]) + Equal(T, `A${PatternBoolean}`) + }) + it('Pattern 7', () => { + const T = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Number()])]) + Equal(T, `(A|${PatternNumber})`) + }) + it('Pattern 8', () => { + const T = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.String()])]) + Equal(T, `(A|${PatternString})`) + }) + it('Pattern 9', () => { + const T = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Boolean()])]) + Equal(T, `(A|${PatternBoolean})`) + }) + // --------------------------------------------------------------- + // Template + // --------------------------------------------------------------- + it('Template 1', () => { + const T = Type.TemplateLiteral([]) + Equal(T, '') + }) + it('Template 2', () => { + // prettier-ignore + const T = Type.TemplateLiteral([ + Type.Literal('A') + ]) + Equal(T, 'A') + }) + it('Template 3', () => { + // prettier-ignore + const T = Type.TemplateLiteral([ + Type.Literal('A'), + Type.Literal('B'), + ]) + Equal(T, 'AB') + }) + it('Template 4', () => { + // prettier-ignore + const T = Type.TemplateLiteral([ + Type.Literal('A'), + Type.Union([ + Type.Literal('B'), + ]) + ]) + Equal(T, 'AB') + }) + it('Template 5', () => { + // prettier-ignore + const T = Type.TemplateLiteral([ + Type.Literal('A'), + Type.Union([ + Type.Literal('B'), + Type.Literal('C'), + ]) + ]) + Equal(T, 'A(B|C)') + }) + it('Template 6', () => { + // prettier-ignore + const T = Type.TemplateLiteral([ + Type.Literal('A'), + Type.Union([ + Type.Literal('B'), + Type.Literal('C'), + ]), + Type.Union([ + Type.Literal('D'), + Type.Literal('E'), + ]) + ]) + Equal(T, 'A(B|C)(D|E)') + }) + it('Template 7', () => { + // prettier-ignore + const T = Type.TemplateLiteral([ + Type.TemplateLiteral([Type.Literal('A')]), + Type.TemplateLiteral([Type.Literal('B')]) + ]) + Equal(T, 'AB') + }) + it('Template 8', () => { + // prettier-ignore + const T = Type.TemplateLiteral([ + Type.Literal('A'), + Type.Union([ + Type.TemplateLiteral([ + Type.Literal('B'), + Type.Literal('C'), + ]), + Type.Literal('D') + ]), + ]) + Equal(T, 'A(BC|D)') + }) + it('Template 9', () => { + // prettier-ignore + const T = Type.TemplateLiteral([ + Type.Literal('A'), + Type.Union([ + Type.TemplateLiteral([ + Type.Literal('B'), + Type.Literal('C'), + ]), + Type.Union([ + Type.Literal('D'), + Type.Literal('E') + ]) + ]), + ]) + Equal(T, 'A(BC|(D|E))') + }) +}) diff --git a/test/runtime/value/assert/assert.ts b/test/runtime/value/assert/assert.ts new file mode 100644 index 000000000..fe3afdce2 --- /dev/null +++ b/test/runtime/value/assert/assert.ts @@ -0,0 +1,35 @@ +import { Value, AssertError } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/Assert', () => { + it('Should Assert', () => { + Assert.Throws(() => Value.Assert(Type.String(), 1)) + }) + it('Should throw AssertError', () => { + try { + Value.Assert(Type.String(), 1) + } catch (error) { + if (error instanceof AssertError) { + return + } + throw error + } + }) + it('Should throw AssertError and produce Iterator', () => { + try { + Value.Assert(Type.String(), 1) + } catch (error) { + if (error instanceof AssertError) { + const first = error.Errors().First() + Assert.HasProperty(first, 'type') + Assert.HasProperty(first, 'schema') + Assert.HasProperty(first, 'path') + Assert.HasProperty(first, 'value') + Assert.HasProperty(first, 'message') + return + } + throw error + } + }) +}) diff --git a/test/runtime/value/assert/index.ts b/test/runtime/value/assert/index.ts new file mode 100644 index 000000000..f043fc641 --- /dev/null +++ b/test/runtime/value/assert/index.ts @@ -0,0 +1 @@ +import './assert' diff --git a/test/runtime/value/cast/any.ts b/test/runtime/value/cast/any.ts new file mode 100644 index 000000000..832caf5f6 --- /dev/null +++ b/test/runtime/value/cast/any.ts @@ -0,0 +1,52 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/cast/Any', () => { + const T = Type.Any() + it('Should upcast from string', () => { + const value = 'hello' + const result = Value.Cast(T, value) + Assert.IsEqual(result, value) + }) + it('Should upcast from number', () => { + const value = 1 + const result = Value.Cast(T, value) + Assert.IsEqual(result, value) + }) + it('Should upcast from boolean', () => { + const value = false + const result = Value.Cast(T, value) + Assert.IsEqual(result, value) + }) + it('Should upcast from object', () => { + const value = {} + const result = Value.Cast(T, value) + Assert.IsEqual(result, value) + }) + it('Should upcast from array', () => { + const value = [1] + const result = Value.Cast(T, value) + Assert.IsEqual(result, value) + }) + it('Should upcast from undefined', () => { + const value = undefined + const result = Value.Cast(T, value) + Assert.IsEqual(result, value) + }) + it('Should upcast from null', () => { + const value = null + const result = Value.Cast(T, value) + Assert.IsEqual(result, value) + }) + it('Should upcast from date', () => { + const value = new Date(100) + const result = Value.Cast(T, value) + Assert.IsEqual(result.getTime(100), 100) + }) + it('Should preserve', () => { + const value = { a: 1, b: 2 } + const result = Value.Cast(T, value) + Assert.IsEqual(result, { a: 1, b: 2 }) + }) +}) diff --git a/test/runtime/value/cast/array.ts b/test/runtime/value/cast/array.ts new file mode 100644 index 000000000..83ad7f6c7 --- /dev/null +++ b/test/runtime/value/cast/array.ts @@ -0,0 +1,118 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/cast/Array', () => { + const T = Type.Array(Type.Number(), { default: [1, 2, 3] }) + const E = [1, 2, 3] + it('Should upcast from string', () => { + const value = 'hello' + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from number', () => { + const value = 1 + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from boolean', () => { + const value = true + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from object', () => { + const value = {} + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from array', () => { + const value = [1] + const result = Value.Cast(T, value) + Assert.IsEqual(result, [1]) + }) + it('Should upcast from undefined', () => { + const value = undefined + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from null', () => { + const value = null + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from date', () => { + const value = new Date(100) + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should preserve', () => { + const value = [6, 7, 8] + const result = Value.Cast(T, value) + Assert.IsEqual(result, [6, 7, 8]) + }) + it('Should preserve with invalid element set to default', () => { + const value = [6, 7, 8, 'hello', 9] + const result = Value.Cast(T, value) + Assert.IsEqual(result, [6, 7, 8, 0, 9]) + }) + // ----------------------------------------------------------------- + // Constraints: Ranges + // ----------------------------------------------------------------- + it('Should cast array and truncate to maxItems from value', () => { + const result = Value.Cast(Type.Array(Type.Number(), { maxItems: 3 }), [0, 1, 2, 4, 5, 6]) + Assert.IsEqual(result, [0, 1, 2]) + }) + it('Should cast arrays and append array to minItems from value', () => { + const result = Value.Cast(Type.Array(Type.Number(), { minItems: 6 }), [0, 1, 2]) + Assert.IsEqual(result, [0, 1, 2, 0, 0, 0]) + }) + it('Should cast array and truncate to maxItems from default value', () => { + const result = Value.Cast(Type.Array(Type.Number(), { maxItems: 3, default: [0, 1, 2, 4, 5, 6] }), null) + Assert.IsEqual(result, [0, 1, 2]) + }) + it('Should cast arrays and append array to minItems from default value', () => { + const result = Value.Cast(Type.Array(Type.Number({ default: 1 }), { minItems: 6, default: [0, 1, 2] }), null) + Assert.IsEqual(result, [0, 1, 2, 1, 1, 1]) + }) + // ----------------------------------------------------------------- + // Constraints: Unique + // ----------------------------------------------------------------- + it('Should cast arrays with uniqueItems with unique default value', () => { + const result = Value.Cast(Type.Array(Type.Number(), { uniqueItems: true, default: [0, 1, 2] }), null) + Assert.IsEqual(result, [0, 1, 2]) + }) + it('Should cast arrays with uniqueItems with unique value', () => { + const result = Value.Cast(Type.Array(Type.Number(), { uniqueItems: true }), [0, 1, 2]) + Assert.IsEqual(result, [0, 1, 2]) + }) + it('Should throw when casting arrays with uniqueItems and no value or default value', () => { + Assert.Throws(() => Value.Cast(Type.Array(Type.Number(), { uniqueItems: true }), null)) + }) + it('Should throw when casting arrays with uniqueItems and not enough values to populate set', () => { + Assert.Throws(() => Value.Cast(Type.Array(Type.Number(), { minItems: 3, uniqueItems: true }), [0, 1])) + }) + it('Should throw when casting arrays with uniqueItems and not enough default values to populate set', () => { + Assert.Throws(() => Value.Cast(Type.Array(Type.Number(), { minItems: 3, uniqueItems: true, default: [0, 1] }), null)) + }) + // ----------------------------------------------------------------- + // Suggestion: https://github.com/sinclairzx81/typebox/issues/239 + // ----------------------------------------------------------------- + it('Should remove duplicates if uniqueItems is true', () => { + const T = Type.Array(Type.Number(), { uniqueItems: true }) + const value = [1, 1, 2, 2] + const result = Value.Cast(T, value) + Assert.IsEqual(result, [1, 2]) + }) + it('Should should fill up with defaults to minItems', () => { + const T = Type.Array(Type.Number(), { minItems: 3 }) + const value = [1, 2] + const result = Value.Cast(T, value) + Assert.IsEqual(result, [1, 2, 0]) + }) + it('Should should truncate to maxItems', () => { + const T = Type.Array(Type.Number(), { maxItems: 3 }) + const value = [1, 2, 3, 4] + const result = Value.Cast(T, value) + Assert.IsEqual(result, [1, 2, 3]) + }) +}) diff --git a/test/runtime/value/cast/async-iterator.ts b/test/runtime/value/cast/async-iterator.ts new file mode 100644 index 000000000..ac27c0971 --- /dev/null +++ b/test/runtime/value/cast/async-iterator.ts @@ -0,0 +1,37 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/cast/AsyncIterator', () => { + const T = Type.AsyncIterator(Type.Any()) + it('Should upcast from string', () => { + const value = 'world' + const result = Value.Cast(T, value) + Assert.IsTrue(Symbol.asyncIterator in result) + }) + it('Should upcast from object', () => { + const value = {} + const result = Value.Cast(T, value) + Assert.IsTrue(Symbol.asyncIterator in result) + }) + it('Should upcast from array', () => { + const value = [1] + const result = Value.Cast(T, value) + Assert.IsTrue(Symbol.asyncIterator in result) + }) + it('Should upcast from undefined', () => { + const value = undefined + const result = Value.Cast(T, value) + Assert.IsTrue(Symbol.asyncIterator in result) + }) + it('Should upcast from null', () => { + const value = null + const result = Value.Cast(T, value) + Assert.IsTrue(Symbol.asyncIterator in result) + }) + it('Should preseve', () => { + const value = (async function* () {})() + const result = Value.Cast(T, value) + Assert.IsTrue(value === result) + }) +}) diff --git a/test/runtime/value/cast/bigint.ts b/test/runtime/value/cast/bigint.ts new file mode 100644 index 000000000..873fa73db --- /dev/null +++ b/test/runtime/value/cast/bigint.ts @@ -0,0 +1,53 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/cast/BigInt', () => { + const T = Type.BigInt() + const E = BigInt(0) + it('Should upcast from string', () => { + const value = 'hello' + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from number', () => { + const value = 0 + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from boolean', () => { + const value = true + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from object', () => { + const value = {} + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from array', () => { + const value = [1] + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from undefined', () => { + const value = undefined + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from null', () => { + const value = null + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from date', () => { + const value = new Date(100) + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should preserve', () => { + const value = BigInt(100) + const result = Value.Cast(T, value) + Assert.IsEqual(result, BigInt(100)) + }) +}) diff --git a/test/runtime/value/cast/boolean.ts b/test/runtime/value/cast/boolean.ts new file mode 100644 index 000000000..cbb976134 --- /dev/null +++ b/test/runtime/value/cast/boolean.ts @@ -0,0 +1,53 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/cast/Boolean', () => { + const T = Type.Boolean() + const E = false + it('Should upcast from string', () => { + const value = 'hello' + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from number', () => { + const value = 0 + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from boolean', () => { + const value = true + const result = Value.Cast(T, value) + Assert.IsEqual(result, true) + }) + it('Should upcast from object', () => { + const value = {} + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from array', () => { + const value = [1] + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from undefined', () => { + const value = undefined + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from null', () => { + const value = null + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from date', () => { + const value = new Date(100) + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should preserve', () => { + const value = true + const result = Value.Cast(T, value) + Assert.IsEqual(result, true) + }) +}) diff --git a/test/runtime/value/cast/composite.ts b/test/runtime/value/cast/composite.ts new file mode 100644 index 000000000..1db6d804e --- /dev/null +++ b/test/runtime/value/cast/composite.ts @@ -0,0 +1,89 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/cast/Composite', () => { + const A = Type.Object({ + x: Type.Number({ default: 0 }), + y: Type.Number({ default: 1 }), + z: Type.Number({ default: 2 }), + }) + const B = Type.Object({ + a: Type.Number({ default: 'a' }), + b: Type.Number({ default: 'b' }), + c: Type.Number({ default: 'c' }), + }) + const T = Type.Composite([A, B]) + const E = { + x: 0, + y: 1, + z: 2, + a: 'a', + b: 'b', + c: 'c', + } + it('Should upcast from string', () => { + const value = 'hello' + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from number', () => { + const value = E + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from boolean', () => { + const value = true + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from object', () => { + const value = {} + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from array', () => { + const value = [1] + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from undefined', () => { + const value = undefined + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from null', () => { + const value = null + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from date', () => { + const value = new Date(100) + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast and preserve object', () => { + const value = { x: 7, y: 8, z: 9 } + const result = Value.Cast(T, value) + Assert.IsEqual(result, { + x: 7, + y: 8, + z: 9, + a: 'a', + b: 'b', + c: 'c', + }) + }) + it('Should upcast and preserve from incorrect properties', () => { + const value = { x: {}, y: 8, z: 9 } + const result = Value.Cast(T, value) + Assert.IsEqual(result, { + x: 0, + y: 8, + z: 9, + a: 'a', + b: 'b', + c: 'c', + }) + }) +}) diff --git a/test/runtime/value/cast/date.ts b/test/runtime/value/cast/date.ts new file mode 100644 index 000000000..e204acf36 --- /dev/null +++ b/test/runtime/value/cast/date.ts @@ -0,0 +1,38 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/cast/Date', () => { + const T = Type.Date() + const E = new Date(0) + it('Should upcast from string', () => { + const value = 'world' + const result = Value.Cast(T, value) + Assert.InRange(result.getTime(), new Date().getTime(), 1000) + }) + it('Should upcast from object', () => { + const value = {} + const result = Value.Cast(T, value) + Assert.InRange(result.getTime(), new Date().getTime(), 1000) + }) + it('Should upcast from array', () => { + const value = [1] + const result = Value.Cast(T, value) + Assert.InRange(result.getTime(), new Date().getTime(), 1000) + }) + it('Should upcast from undefined', () => { + const value = undefined + const result = Value.Cast(T, value) + Assert.InRange(result.getTime(), new Date().getTime(), 1000) + }) + it('Should upcast from null', () => { + const value = null + const result = Value.Cast(T, value) + Assert.InRange(result.getTime(), new Date().getTime(), 1000) + }) + it('Should preseve', () => { + const value = new Date(100) + const result = Value.Cast(T, value) + Assert.IsEqual(result.getTime(), 100) + }) +}) diff --git a/test/runtime/value/cast/enum.ts b/test/runtime/value/cast/enum.ts new file mode 100644 index 000000000..0013e8119 --- /dev/null +++ b/test/runtime/value/cast/enum.ts @@ -0,0 +1,62 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/cast/Boolean', () => { + enum Foo { + A, + B, + } + const T = Type.Enum(Foo) + const E = Foo.A + it('Should upcast from string', () => { + const value = 'hello' + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from number', () => { + const value = 123 + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from boolean', () => { + const value = true + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from object', () => { + const value = {} + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from array', () => { + const value = [1] + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from undefined', () => { + const value = undefined + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from null', () => { + const value = null + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from date', () => { + const value = new Date(100) + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from enum A', () => { + const value = Foo.A + const result = Value.Cast(T, value) + Assert.IsEqual(result, Foo.A) + }) + it('Should upcast from enum B', () => { + const value = Foo.B + const result = Value.Cast(T, value) + Assert.IsEqual(result, Foo.B) + }) +}) diff --git a/test/runtime/value/cast/import.ts b/test/runtime/value/cast/import.ts new file mode 100644 index 000000000..56a5a9236 --- /dev/null +++ b/test/runtime/value/cast/import.ts @@ -0,0 +1,51 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/cast/Import', () => { + const T = Type.Module({ + A: Type.Number(), + }).Import('A') + + const E = 0 + it('Should upcast from string', () => { + const value = 'world' + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from number', () => { + const value = 1 + const result = Value.Cast(T, value) + Assert.IsEqual(result, 1) + }) + it('Should upcast from object', () => { + const value = {} + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from array', () => { + const value = [1] + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from undefined', () => { + const value = undefined + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from null', () => { + const value = null + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from date', () => { + const value = new Date(100) + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should preseve', () => { + const value = 123 + const result = Value.Cast(T, value) + Assert.IsEqual(result, 123) + }) +}) diff --git a/test/runtime/value/cast/index.ts b/test/runtime/value/cast/index.ts new file mode 100644 index 000000000..0016139b8 --- /dev/null +++ b/test/runtime/value/cast/index.ts @@ -0,0 +1,32 @@ +import './any' +import './array' +import './async-iterator' +import './bigint' +import './boolean' +import './composite' +import './date' +import './enum' +import './import' +import './integer' +import './intersect' +import './iterator' +import './keyof' +import './kind' +import './literal' +import './never' +import './not' +import './null' +import './number' +import './object' +import './recursive' +import './record' +import './regexp' +import './string' +import './symbol' +import './template-literal' +import './tuple' +import './uint8array' +import './undefined' +import './union' +import './unknown' +import './void' diff --git a/test/runtime/value/cast/integer.ts b/test/runtime/value/cast/integer.ts new file mode 100644 index 000000000..3a5b375c1 --- /dev/null +++ b/test/runtime/value/cast/integer.ts @@ -0,0 +1,43 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/cast/Integer', () => { + const T = Type.Integer() + const E = 0 + it('Should upcast from string', () => { + const value = 'hello' + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from number', () => { + const value = 1 + const result = Value.Cast(T, value) + Assert.IsEqual(result, 1) + }) + it('Should upcast from object', () => { + const value = {} + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from array', () => { + const value = [1] + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from undefined', () => { + const value = undefined + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from null', () => { + const value = null + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from date', () => { + const value = new Date(100) + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) +}) diff --git a/test/runtime/value/cast/intersect.ts b/test/runtime/value/cast/intersect.ts new file mode 100644 index 000000000..bed80df65 --- /dev/null +++ b/test/runtime/value/cast/intersect.ts @@ -0,0 +1,112 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/cast/Intersect', () => { + it('Should cast from an invalid object', () => { + // prettier-ignore + const T = Type.Intersect([ + Type.Object({ x: Type.Number() }), + Type.Object({ y: Type.Number() }) + ]) + const V = Value.Cast(T, 1) + Assert.IsEqual(V, { x: 0, y: 0 }) + }) + it('Should cast from an partial object and preserve', () => { + // prettier-ignore + const T = Type.Intersect([ + Type.Object({ x: Type.Number() }), + Type.Object({ y: Type.Number() }) + ]) + const V = Value.Cast(T, { x: 1 }) + Assert.IsEqual(V, { x: 1, y: 0 }) + }) + it('Should cast and use default values', () => { + // prettier-ignore + const T = Type.Intersect([ + Type.Object({ x: Type.Number() }), + Type.Object({ y: Type.Number({ default: 42 }) }) + ]) + const V = Value.Cast(T, { x: 1 }) + Assert.IsEqual(V, { x: 1, y: 42 }) + }) + it('Should throw with an illogical intersect', () => { + // prettier-ignore + const T = Type.Intersect([ + Type.Object({ x: Type.Number() }), + Type.Object({ x: Type.String() }) + ]) + Assert.Throws(() => Value.Cast(T, { x: 1 })) + }) + it('Should throw with an illogical intersect (primative)', () => { + // prettier-ignore + const T = Type.Intersect([ + Type.Number(), + Type.String() + ]) + Assert.Throws(() => Value.Cast(T, { x: 1 })) + }) + it('Should use last intersected default for equivalent sub schemas', () => { + // prettier-ignore + const T = Type.Intersect([ + Type.Object({ x: Type.Number() }), + Type.Object({ x: Type.Number({ default: 1000 }) }) + ]) + const V = Value.Cast(T, null) + Assert.IsEqual(V, { x: 1000 }) + }) + it('Should use last intersected default for equivalent sub schemas (primitives)', () => { + // prettier-ignore + const T = Type.Intersect([ + Type.Number(), + Type.Number({ default: 1000 }) + ]) + const V = Value.Cast(T, null) + Assert.IsEqual(V, 1000) + }) + it('Should preserve if default is specified', () => { + // prettier-ignore + const T = Type.Intersect([ + Type.Number(), + Type.Number({ default: 1000 }) + ]) + const V = Value.Cast(T, 2000) + Assert.IsEqual(V, 2000) + }) + + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/1264 + // ---------------------------------------------------------------- + it('Should preserve intersected properties', () => { + const T = Type.Intersect([ + Type.Object({}), + Type.Object({ + name: Type.String(), + age: Type.Optional(Type.Number()), + location: Type.Object({ + lat: Type.Number(), + long: Type.Number(), + }), + greeting: Type.String(), + }), + ]) + const V0 = Value.Cast(T, { greeting: 'Hello' }) + const V1 = Value.Cast(T, { location: null, greeting: 'Hello' }) + const V2 = Value.Cast(T, { location: { lat: 1 }, greeting: 'Hello' }) + const V3 = Value.Cast(T, { location: { lat: 1, long: 1 }, greeting: 'Hello' }) + + Assert.IsEqual(V0, { name: '', location: { lat: 0, long: 0 }, greeting: 'Hello' }) + Assert.IsEqual(V1, { name: '', location: { lat: 0, long: 0 }, greeting: 'Hello' }) + Assert.IsEqual(V2, { name: '', location: { lat: 1, long: 0 }, greeting: 'Hello' }) + Assert.IsEqual(V3, { name: '', location: { lat: 1, long: 1 }, greeting: 'Hello' }) + }) + + // -------------------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/1269#issuecomment-2993924180 + // -------------------------------------------------------------------------- + it('Should Cast with intersected Record', () => { + const T = Type.Intersect([Type.Record(Type.TemplateLiteral('x-${string}'), Type.Unknown()), Type.Object({ name: Type.String() })]) + const R = Value.Cast(T, {}) + Assert.IsEqual(R, { name: '' }) + }) +}) diff --git a/test/runtime/value/cast/iterator.ts b/test/runtime/value/cast/iterator.ts new file mode 100644 index 000000000..14863180c --- /dev/null +++ b/test/runtime/value/cast/iterator.ts @@ -0,0 +1,37 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/cast/Iterator', () => { + const T = Type.Iterator(Type.Any()) + it('Should upcast from string', () => { + const value = 'world' + const result = Value.Cast(T, value) + Assert.IsTrue(Symbol.iterator in result) + }) + it('Should upcast from object', () => { + const value = {} + const result = Value.Cast(T, value) + Assert.IsTrue(Symbol.iterator in result) + }) + it('Should upcast from array', () => { + const value = [1] + const result = Value.Cast(T, value) + Assert.IsTrue(Symbol.iterator in result) + }) + it('Should upcast from undefined', () => { + const value = undefined + const result = Value.Cast(T, value) + Assert.IsTrue(Symbol.iterator in result) + }) + it('Should upcast from null', () => { + const value = null + const result = Value.Cast(T, value) + Assert.IsTrue(Symbol.iterator in result) + }) + it('Should preseve', () => { + const value = (function* () {})() + const result = Value.Cast(T, value) + Assert.IsTrue(value === result) + }) +}) diff --git a/test/runtime/value/cast/keyof.ts b/test/runtime/value/cast/keyof.ts new file mode 100644 index 000000000..d0f41daf7 --- /dev/null +++ b/test/runtime/value/cast/keyof.ts @@ -0,0 +1,59 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/cast/KeyOf', () => { + const T = Type.KeyOf( + Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }), + ) + const E = 'x' + it('Should upcast from string', () => { + const value = 'hello' + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from number', () => { + const value = 1 + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from boolean', () => { + const value = true + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from object', () => { + const value = {} + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from array', () => { + const value = [1] + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from undefined', () => { + const value = undefined + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from null', () => { + const value = null + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from date', () => { + const value = new Date(100) + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should preserve', () => { + const value = 'y' + const result = Value.Cast(T, value) + Assert.IsEqual(result, 'y') + }) +}) diff --git a/test/runtime/value/cast/kind.ts b/test/runtime/value/cast/kind.ts new file mode 100644 index 000000000..42cffc618 --- /dev/null +++ b/test/runtime/value/cast/kind.ts @@ -0,0 +1,62 @@ +import { Value } from '@sinclair/typebox/value' +import { Type, Kind, TypeRegistry } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/cast/Kind', () => { + // --------------------------------------------------------- + // Fixtures + // --------------------------------------------------------- + before(() => TypeRegistry.Set('Kind', (schema, value) => value === 'hello' || value === 'world')) + after(() => TypeRegistry.Clear()) + // --------------------------------------------------------- + // Tests + // --------------------------------------------------------- + const T = Type.Unsafe({ [Kind]: 'Kind', default: 'hello' }) + const E = 'hello' + it('Should upcast from string', () => { + const value = 'hello' + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from number', () => { + const value = 1 + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from boolean', () => { + const value = false + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from object', () => { + const value = {} + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from array', () => { + const value = [1] + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from undefined', () => { + const value = undefined + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from null', () => { + const value = null + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should preserve', () => { + const value = { a: 'hello', b: 'world' } + const result = Value.Cast( + Type.Object({ + a: T, + b: T, + }), + value, + ) + Assert.IsEqual(result, { a: 'hello', b: 'world' }) + }) +}) diff --git a/test/runtime/value/cast/literal.ts b/test/runtime/value/cast/literal.ts new file mode 100644 index 000000000..59b356908 --- /dev/null +++ b/test/runtime/value/cast/literal.ts @@ -0,0 +1,53 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/cast/Literal', () => { + const T = Type.Literal('hello') + const E = 'hello' + it('Should upcast from string', () => { + const value = 'world' + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from number', () => { + const value = 1 + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from boolean', () => { + const value = true + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from object', () => { + const value = {} + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from array', () => { + const value = [1] + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from undefined', () => { + const value = undefined + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from null', () => { + const value = null + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from date', () => { + const value = new Date(100) + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should preseve', () => { + const value = 'hello' + const result = Value.Cast(T, value) + Assert.IsEqual(result, 'hello') + }) +}) diff --git a/test/runtime/value/cast/never.ts b/test/runtime/value/cast/never.ts new file mode 100644 index 000000000..eeb782ed8 --- /dev/null +++ b/test/runtime/value/cast/never.ts @@ -0,0 +1,39 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/cast/Never', () => { + const T = Type.Never() + it('Should throw from string', () => { + const value = 'hello' + Assert.Throws(() => Value.Cast(T, value)) + }) + it('Should throw from number', () => { + const value = 1 + Assert.Throws(() => Value.Cast(T, value)) + }) + it('Should throw from boolean', () => { + const value = false + Assert.Throws(() => Value.Cast(T, value)) + }) + it('Should throw from object', () => { + const value = {} + Assert.Throws(() => Value.Cast(T, value)) + }) + it('Should throw from array', () => { + const value = [1] + Assert.Throws(() => Value.Cast(T, value)) + }) + it('Should throw from undefined', () => { + const value = undefined + Assert.Throws(() => Value.Cast(T, value)) + }) + it('Should throw from null', () => { + const value = null + Assert.Throws(() => Value.Cast(T, value)) + }) + it('Should upcast from date', () => { + const value = null + Assert.Throws(() => Value.Cast(T, value)) + }) +}) diff --git a/test/runtime/value/cast/not.ts b/test/runtime/value/cast/not.ts new file mode 100644 index 000000000..d5802e646 --- /dev/null +++ b/test/runtime/value/cast/not.ts @@ -0,0 +1,52 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/cast/Not', () => { + const T = Type.Not(Type.String(), { default: 0 }) + it('Should upcast from string', () => { + const value = 'hello' + const result = Value.Cast(T, value) + Assert.IsEqual(result, 0) // default + }) + it('Should upcast from number', () => { + const value = 0 + const result = Value.Cast(T, value) + Assert.IsEqual(result, value) + }) + it('Should upcast from boolean', () => { + const value = true + const result = Value.Cast(T, value) + Assert.IsEqual(result, value) + }) + it('Should upcast from object', () => { + const value = {} + const result = Value.Cast(T, value) + Assert.IsEqual(result, value) + }) + it('Should upcast from array', () => { + const value = [1] + const result = Value.Cast(T, value) + Assert.IsEqual(result, value) + }) + it('Should upcast from undefined', () => { + const value = undefined + const result = Value.Cast(T, value) + Assert.IsEqual(result, value) + }) + it('Should upcast from null', () => { + const value = null + const result = Value.Cast(T, value) + Assert.IsEqual(result, value) + }) + it('Should upcast from date', () => { + const value = new Date(100) + const result = Value.Cast(T, value) + Assert.IsEqual(result, value) + }) + it('Should preserve', () => { + const value = 100 + const result = Value.Cast(T, value) + Assert.IsEqual(result, 100) + }) +}) diff --git a/test/runtime/value/cast/null.ts b/test/runtime/value/cast/null.ts new file mode 100644 index 000000000..de1d5a737 --- /dev/null +++ b/test/runtime/value/cast/null.ts @@ -0,0 +1,53 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/cast/Null', () => { + const T = Type.Null() + const E = null + it('Should upcast from string', () => { + const value = 'world' + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from number', () => { + const value = 1 + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from boolean', () => { + const value = true + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from object', () => { + const value = {} + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from array', () => { + const value = [1] + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from undefined', () => { + const value = undefined + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from null', () => { + const value = null + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from date', () => { + const value = new Date(100) + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should preseve', () => { + const value = null + const result = Value.Cast(T, value) + Assert.IsEqual(result, null) + }) +}) diff --git a/test/runtime/value/cast/number.ts b/test/runtime/value/cast/number.ts new file mode 100644 index 000000000..882ad6474 --- /dev/null +++ b/test/runtime/value/cast/number.ts @@ -0,0 +1,48 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/cast/Number', () => { + const T = Type.Number() + const E = 0 + it('Should upcast from string', () => { + const value = 'world' + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from number', () => { + const value = 1 + const result = Value.Cast(T, value) + Assert.IsEqual(result, 1) + }) + it('Should upcast from object', () => { + const value = {} + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from array', () => { + const value = [1] + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from undefined', () => { + const value = undefined + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from null', () => { + const value = null + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from date', () => { + const value = new Date(100) + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should preseve', () => { + const value = 123 + const result = Value.Cast(T, value) + Assert.IsEqual(result, 123) + }) +}) diff --git a/test/runtime/value/cast/object.ts b/test/runtime/value/cast/object.ts new file mode 100644 index 000000000..56a0668de --- /dev/null +++ b/test/runtime/value/cast/object.ts @@ -0,0 +1,162 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/cast/Object', () => { + const T = Type.Object({ + a: Type.Number({ default: 'a' }), + b: Type.Number({ default: 'b' }), + c: Type.Number({ default: 'c' }), + x: Type.Number({ default: 0 }), + y: Type.Number({ default: 1 }), + z: Type.Number({ default: 2 }), + }) + const E = { + x: 0, + y: 1, + z: 2, + a: 'a', + b: 'b', + c: 'c', + } + it('Should upcast from string', () => { + const value = 'hello' + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from number', () => { + const value = E + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from boolean', () => { + const value = true + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from object', () => { + const value = {} + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from array', () => { + const value = [1] + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from undefined', () => { + const value = undefined + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from null', () => { + const value = null + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from date', () => { + const value = new Date(100) + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should preserve', () => { + const value = { x: 7, y: 8, z: 9, a: 10, b: 11, c: 12 } + const result = Value.Cast(T, value) + Assert.IsEqual(result, { + x: 7, + y: 8, + z: 9, + a: 10, + b: 11, + c: 12, + }) + }) + it('Should upcast and preserve partial object', () => { + const value = { x: 7, y: 8, z: 9 } + const result = Value.Cast(T, value) + Assert.IsEqual(result, { + x: 7, + y: 8, + z: 9, + a: 'a', + b: 'b', + c: 'c', + }) + }) + it('Should upcast and preserve partial object with incorrect properties', () => { + const value = { x: {}, y: 8, z: 9 } + const result = Value.Cast(T, value) + Assert.IsEqual(result, { + x: 0, + y: 8, + z: 9, + a: 'a', + b: 'b', + c: 'c', + }) + }) + it('Should upcast and preserve partial object and omit unknown properties', () => { + const value = { x: 7, y: 8, z: 9, unknown: 'foo' } + const result = Value.Cast(T, value) + Assert.IsEqual(result, { + x: 7, + y: 8, + z: 9, + a: 'a', + b: 'b', + c: 'c', + }) + }) + it('Should upcast and create invalid additional properties', () => { + const result = Value.Cast( + Type.Object( + { + x: Type.Number(), + y: Type.Number(), + }, + { + additionalProperties: Type.Object({ + a: Type.Number(), + b: Type.Number(), + }), + }, + ), + { + x: 1, + y: 2, + z: true, + }, + ) + Assert.IsEqual(result, { + x: 1, + y: 2, + z: { a: 0, b: 0 }, + }) + }) + it('Should upcast and preserve additional properties', () => { + const result = Value.Cast( + Type.Object( + { + x: Type.Number(), + y: Type.Number(), + }, + { + additionalProperties: Type.Object({ + a: Type.Number(), + b: Type.Number(), + }), + }, + ), + { + x: 1, + y: 2, + z: { b: 1 }, + }, + ) + Assert.IsEqual(result, { + x: 1, + y: 2, + z: { a: 0, b: 1 }, + }) + }) +}) diff --git a/test/runtime/value/cast/record.ts b/test/runtime/value/cast/record.ts new file mode 100644 index 000000000..fa6ea9d72 --- /dev/null +++ b/test/runtime/value/cast/record.ts @@ -0,0 +1,80 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/cast/Record', () => { + const T = Type.Record( + Type.String(), + Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }), + ) + const E = {} + it('Should upcast from string', () => { + const value = 'hello' + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from number', () => { + const value = E + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from boolean', () => { + const value = true + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from object', () => { + const value = {} + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from array', () => { + const value = [1] + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from undefined', () => { + const value = undefined + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from null', () => { + const value = null + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from date', () => { + const value = new Date(100) + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should preserve', () => { + const value = { + a: { x: 1, y: 2, z: 3 }, + b: { x: 4, y: 5, z: 6 }, + } + const result = Value.Cast(T, value) + Assert.IsEqual(result, value) + }) + it('Should preserve and patch invalid records', () => { + const value = { + a: { x: 1, y: 2, z: 3 }, + b: { x: 4, y: 5, z: {} }, + c: [1, 2, 3], + d: 1, + e: { x: 1, y: 2, w: 9000 }, + } + const result = Value.Cast(T, value) + Assert.IsEqual(result, { + a: { x: 1, y: 2, z: 3 }, + b: { x: 4, y: 5, z: 0 }, + c: { x: 0, y: 0, z: 0 }, + d: { x: 0, y: 0, z: 0 }, + e: { x: 1, y: 2, z: 0 }, + }) + }) +}) diff --git a/test/runtime/value/cast/recursive.ts b/test/runtime/value/cast/recursive.ts new file mode 100644 index 000000000..cb2616043 --- /dev/null +++ b/test/runtime/value/cast/recursive.ts @@ -0,0 +1,98 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/cast/Recursive', () => { + const T = Type.Recursive((This) => + Type.Object({ + id: Type.String(), + nodes: Type.Array(This), + }), + ) + const E = { id: '', nodes: [] } + it('Should upcast from string', () => { + const value = 'hello' + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from number', () => { + const value = E + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from boolean', () => { + const value = true + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from object', () => { + const value = {} + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from array', () => { + const value = [1] + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from undefined', () => { + const value = undefined + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from null', () => { + const value = null + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from date', () => { + const value = new Date(100) + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should preserve', () => { + const value = { + id: 'A', + nodes: [ + { id: 'B', nodes: [] }, + { id: 'C', nodes: [] }, + { id: 'D', nodes: [] }, + ], + } + const result = Value.Cast(T, value) + Assert.IsEqual(result, value) + }) + it('Should upcast from varying types', () => { + const TypeA = Type.Recursive((This) => + Type.Object({ + id: Type.String(), + nodes: Type.Array(This), + }), + ) + const TypeB = Type.Recursive((This) => + Type.Object({ + id: Type.String(), + name: Type.String({ default: 'test' }), + nodes: Type.Array(This), + }), + ) + const ValueA = { + id: 'A', + nodes: [ + { id: 'B', nodes: [] }, + { id: 'C', nodes: [] }, + { id: 'D', nodes: [] }, + ], + } + const ValueB = Value.Cast(TypeB, ValueA) + // Assert.isEqual(ValueB, { + // id: 'A', + // name: 'test', + // nodes: [ + // { id: 'B', name: 'test', nodes: [] }, + // { id: 'C', name: 'test', nodes: [] }, + // { id: 'D', name: 'test', nodes: [] }, + // ], + // }) + }) +}) diff --git a/test/runtime/value/cast/regexp.ts b/test/runtime/value/cast/regexp.ts new file mode 100644 index 000000000..6c036fa9a --- /dev/null +++ b/test/runtime/value/cast/regexp.ts @@ -0,0 +1,60 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/cast/RegExp', () => { + const T = Type.RegExp(/foo/, { default: 'foo' }) + const E = 'foo' + it('Should upcast from string', () => { + const value = 'hello' + const result = Value.Cast(T, value) + Assert.IsEqual(result, 'foo') + }) + it('Should upcast from number', () => { + const value = 1 + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from boolean', () => { + const value = true + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from object', () => { + const value = {} + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from array', () => { + const value = [1] + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from undefined', () => { + const value = undefined + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from null', () => { + const value = null + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from date', () => { + const value = new Date(100) + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should preserve', () => { + const value = 'foo' + const result = Value.Cast(T, value) + Assert.IsEqual(result, value) + }) + // ---------------------------------------------------------------- + // Throw + // ---------------------------------------------------------------- + it('Should throw with no default', () => { + const T = Type.RegExp(/foo/) + Assert.Throws(() => Value.Cast(T, null)) + }) +}) diff --git a/test/runtime/value/cast/string.ts b/test/runtime/value/cast/string.ts new file mode 100644 index 000000000..24d3fd6ab --- /dev/null +++ b/test/runtime/value/cast/string.ts @@ -0,0 +1,43 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/cast/String', () => { + const T = Type.String() + const E = '' + it('Should upcast from string', () => { + const value = 'hello' + const result = Value.Cast(T, value) + Assert.IsEqual(result, 'hello') + }) + it('Should upcast from object', () => { + const value = {} + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from array', () => { + const value = [1] + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from undefined', () => { + const value = undefined + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from null', () => { + const value = null + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from date', () => { + const value = new Date(100) + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should preserve', () => { + const value = 'foo' + const result = Value.Cast(T, value) + Assert.IsEqual(result, value) + }) +}) diff --git a/test/runtime/value/cast/symbol.ts b/test/runtime/value/cast/symbol.ts new file mode 100644 index 000000000..18598e809 --- /dev/null +++ b/test/runtime/value/cast/symbol.ts @@ -0,0 +1,52 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/cast/Symbol', () => { + const T = Type.Symbol() + it('Should upcast from string', () => { + const value = 'hello' + const result = Value.Cast(T, value) + Assert.IsTypeOf(result, 'symbol') + }) + it('Should upcast from number', () => { + const value = 0 + const result = Value.Cast(T, value) + Assert.IsTypeOf(result, 'symbol') + }) + it('Should upcast from boolean', () => { + const value = true + const result = Value.Cast(T, value) + Assert.IsTypeOf(result, 'symbol') + }) + it('Should upcast from object', () => { + const value = {} + const result = Value.Cast(T, value) + Assert.IsTypeOf(result, 'symbol') + }) + it('Should upcast from array', () => { + const value = [1] + const result = Value.Cast(T, value) + Assert.IsTypeOf(result, 'symbol') + }) + it('Should upcast from undefined', () => { + const value = undefined + const result = Value.Cast(T, value) + Assert.IsTypeOf(result, 'symbol') + }) + it('Should upcast from null', () => { + const value = null + const result = Value.Cast(T, value) + Assert.IsTypeOf(result, 'symbol') + }) + it('Should upcast from date', () => { + const value = new Date(100) + const result = Value.Cast(T, value) + Assert.IsTypeOf(result, 'symbol') + }) + it('Should preserve', () => { + const value = Symbol('hello') + const result = Value.Cast(T, value) + Assert.IsEqual(result.description, value.description) + }) +}) diff --git a/test/runtime/value/cast/template-literal.ts b/test/runtime/value/cast/template-literal.ts new file mode 100644 index 000000000..dc199e17a --- /dev/null +++ b/test/runtime/value/cast/template-literal.ts @@ -0,0 +1,53 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/cast/TemplateLiteral', () => { + const T = Type.TemplateLiteral([Type.Literal('hello'), Type.Literal('world')]) + const E = 'helloworld' + it('Should upcast from string', () => { + const value = 'world' + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from number', () => { + const value = 1 + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from boolean', () => { + const value = true + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from object', () => { + const value = {} + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from array', () => { + const value = [1] + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from undefined', () => { + const value = undefined + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from null', () => { + const value = null + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from date', () => { + const value = new Date(100) + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should preseve', () => { + const value = 'helloworld' + const result = Value.Cast(T, value) + Assert.IsEqual(result, 'helloworld') + }) +}) diff --git a/test/runtime/value/cast/tuple.ts b/test/runtime/value/cast/tuple.ts new file mode 100644 index 000000000..8d30bbed5 --- /dev/null +++ b/test/runtime/value/cast/tuple.ts @@ -0,0 +1,73 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/cast/Tuple', () => { + const T = Type.Tuple([Type.Number(), Type.String()]) + const E = [0, ''] + it('Should upcast from string', () => { + const value = 'hello' + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from number', () => { + const value = 1 + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from boolean', () => { + const value = true + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from object', () => { + const value = {} + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from array', () => { + const value = [1] + const result = Value.Cast(T, value) + Assert.IsEqual(result, [1, '']) + }) + it('Should upcast from undefined', () => { + const value = undefined + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from null', () => { + const value = null + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from date', () => { + const value = new Date(100) + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should preserve', () => { + const value = [42, 'world'] + const result = Value.Cast(T, value) + Assert.IsEqual(result, value) + }) + it('Should upcast with empty', () => { + const value = [] as any[] + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should append with less than tuple length', () => { + const value = [42] + const result = Value.Cast(T, value) + Assert.IsEqual(result, [42, '']) + }) + it('Should truncate with greater than tuple length', () => { + const value = [42, '', true] + const result = Value.Cast(T, value) + Assert.IsEqual(result, [42, '']) + }) + it('Should preserve and patch invalid element', () => { + const value = [{}, 'hello'] + const result = Value.Cast(T, value) + Assert.IsEqual(result, [0, 'hello']) + }) +}) diff --git a/test/runtime/value/cast/uint8array.ts b/test/runtime/value/cast/uint8array.ts new file mode 100644 index 000000000..6678cc715 --- /dev/null +++ b/test/runtime/value/cast/uint8array.ts @@ -0,0 +1,53 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/cast/Uint8Array', () => { + const T = Type.Uint8Array({ default: new Uint8Array([0, 1, 2, 3]) }) + const E = new Uint8Array([0, 1, 2, 3]) + it('Should upcast from string', () => { + const value = 'hello' + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from number', () => { + const value = 1 + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from boolean', () => { + const value = true + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from object', () => { + const value = {} + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from array', () => { + const value = [1] + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from undefined', () => { + const value = undefined + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from null', () => { + const value = null + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from date', () => { + const value = new Date(100) + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should preserve', () => { + const value = new Uint8Array([6, 7, 8, 9, 10]) + const result = Value.Cast(T, value) + Assert.IsEqual(result, value) + }) +}) diff --git a/test/runtime/value/cast/undefined.ts b/test/runtime/value/cast/undefined.ts new file mode 100644 index 000000000..8bb23a4fe --- /dev/null +++ b/test/runtime/value/cast/undefined.ts @@ -0,0 +1,53 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/cast/Undefined', () => { + const T = Type.Undefined() + const E = undefined + it('Should upcast from string', () => { + const value = 'world' + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from number', () => { + const value = 1 + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from boolean', () => { + const value = true + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from object', () => { + const value = {} + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from array', () => { + const value = [1] + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from undefined', () => { + const value = undefined + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from null', () => { + const value = null + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from date', () => { + const value = new Date(100) + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should preseve', () => { + const value = undefined + const result = Value.Cast(T, value) + Assert.IsEqual(result, undefined) + }) +}) diff --git a/test/runtime/value/cast/union.ts b/test/runtime/value/cast/union.ts new file mode 100644 index 000000000..dcc16ceef --- /dev/null +++ b/test/runtime/value/cast/union.ts @@ -0,0 +1,435 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/cast/Union', () => { + const A = Type.Object( + { + type: Type.Literal('A'), + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }, + { additionalProperties: false }, + ) + const B = Type.Object( + { + type: Type.Literal('B'), + a: Type.String(), + b: Type.String(), + c: Type.String(), + }, + { additionalProperties: false }, + ) + const T = Type.Union([A, B]) + const E = { + type: 'A', + x: 0, + y: 0, + z: 0, + } + it('Should upcast from string', () => { + const value = 'hello' + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from number', () => { + const value = 1 + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from boolean', () => { + const value = true + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from object', () => { + const value = {} + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from array', () => { + const value = [1] + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from undefined', () => { + const value = undefined + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from null', () => { + const value = null + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from date', () => { + const value = new Date(100) + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should preserve A', () => { + const value = { type: 'A', x: 1, y: 2, z: 3 } + const result = Value.Cast(T, value) + Assert.IsEqual(result, value) + }) + it('Should preserve B', () => { + const value = { type: 'B', a: 'a', b: 'b', c: 'c' } + const result = Value.Cast(T, value) + Assert.IsEqual(result, value) + }) + it('Should infer through heuristics #1', () => { + const value = { type: 'A', a: 'a', b: 'b', c: 'c' } + const result = Value.Cast(T, value) + Assert.IsEqual(result, { type: 'A', x: 0, y: 0, z: 0 }) + }) + it('Should infer through heuristics #2', () => { + const value = { type: 'B', x: 1, y: 2, z: 3 } + const result = Value.Cast(T, value) + Assert.IsEqual(result, { type: 'B', a: '', b: '', c: '' }) + }) + it('Should infer through heuristics #3', () => { + const value = { type: 'A', a: 'a', b: 'b', c: null } + const result = Value.Cast(T, value) + Assert.IsEqual(result, { type: 'A', x: 0, y: 0, z: 0 }) + }) + it('Should infer through heuristics #4', () => { + const value = { type: 'B', x: 1, y: 2, z: {} } + const result = Value.Cast(T, value) + Assert.IsEqual(result, { type: 'B', a: '', b: '', c: '' }) + }) + it('Should infer through heuristics #5', () => { + const value = { type: 'B', x: 1, y: 2, z: null } + const result = Value.Cast(T, value) + Assert.IsEqual(result, { type: 'B', a: '', b: '', c: '' }) + }) + it('Should infer through heuristics #6', () => { + const value = { x: 1 } + const result = Value.Cast(T, value) + Assert.IsEqual(result, { type: 'A', x: 1, y: 0, z: 0 }) + }) + it('Should infer through heuristics #7', () => { + const value = { a: null } // property existing should contribute + const result = Value.Cast(T, value) + Assert.IsEqual(result, { type: 'B', a: '', b: '', c: '' }) + }) + it('Should cast with default value (create)', () => { + const result = Value.Cast( + Type.Object({ + id: Type.Number(), + value: Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')], { default: 'C' }), + }), + { + id: 42, + value: 'D', + }, + ) + Assert.IsEqual(result, { + id: 42, + value: 'C', + }) + }) + it('Should cast with default value (preserve)', () => { + const result = Value.Cast( + Type.Object({ + id: Type.Number(), + value: Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')], { default: 'C' }), + }), + { + id: 42, + value: 'B', + }, + ) + Assert.IsEqual(result, { + id: 42, + value: 'B', + }) + }) + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/880 + // ---------------------------------------------------------------- + // prettier-ignore + it('Should dereference union variants', () => { + const A = Type.Object({ type: Type.Literal('A') }, { $id: 'A' }) + const B = Type.Object({ type: Type.Literal('B'), value: Type.Number() }, { $id: 'B' }) + const RA = Type.Union([A, B]) + const RB = Type.Union([Type.Ref('A'), Type.Ref('B')]) + // variant 0 + Assert.IsEqual(Value.Cast(RA, [A, B], { type: 'B' }), { type: 'B', value: 0 }) + Assert.IsEqual(Value.Cast(RB, [A, B], { type: 'B' }), { type: 'B', value: 0 }) + // variant 1 + Assert.IsEqual(Value.Cast(RA, [A, B], { type: 'A' }), { type: 'A' }) + Assert.IsEqual(Value.Cast(RB, [A, B], { type: 'A' }), { type: 'A' }) + }) + + // ------------------------------------------------------------------------ + // ref: https://github.com/sinclairzx81/typebox/issues/1268 + // ------------------------------------------------------------------------ + it('should correctly score nested union types #1', () => { + const A = Type.Union([ + Type.Union([ + Type.Object({ + type: Type.Literal('a'), + name: Type.String(), + in: Type.String(), + }), + Type.Object({ + type: Type.Literal('b'), + description: Type.Optional(Type.String()), + nested: Type.Object({ + a: Type.String(), + b: Type.Optional(Type.String()), + }), + }), + ]), + Type.Object({ + $ref: Type.String(), + description: Type.Optional(Type.String()), + }), + ]) + + Assert.IsEqual( + Value.Cast(A, { + type: 'b', + description: 'Hello World', + nested: { + b: 'hello', + }, + }), + { + type: 'b', + description: 'Hello World', + nested: { a: '', b: 'hello' }, + }, + ) + }) + + it('should correctly score nested union types #2', () => { + const A = Type.Union([ + Type.Union([ + Type.Object({ + prop1: Type.String(), + prop2: Type.String(), + prop3: Type.String(), + }), + Type.Object({ + prop1: Type.String(), + prop4: Type.String(), + prop5: Type.String(), + }), + ]), + Type.Union([ + Type.Object({ + prop6: Type.String(), + prop7: Type.String(), + prop8: Type.String(), + }), + Type.Object({ + prop1: Type.String(), + prop9: Type.String(), + prop10: Type.String(), + }), + ]), + ]) + + // Picks the first union variant when the score is equal + Assert.IsEqual( + Value.Cast(A, { + prop1: '', + }), + { + prop1: '', + prop2: '', + prop3: '', + }, + ) + + Assert.IsEqual( + Value.Cast(A, { + prop1: '', + prop4: '', + }), + { + prop1: '', + prop4: '', + prop5: '', + }, + ) + + Assert.IsEqual( + Value.Cast(A, { + prop6: '', + }), + { + prop6: '', + prop7: '', + prop8: '', + }, + ) + }) + + it('should correctly score nested union types #3', () => { + const A = Type.Union([ + Type.Object({ + prop1: Type.String(), + prop2: Type.String(), + prop3: Type.String(), + }), + Type.Object({ + prop4: Type.String(), + prop5: Type.String(), + prop6: Type.String(), + }), + Type.Union([ + Type.Object({ + prop4: Type.String(), + prop5: Type.String(), + prop6: Type.String(), + }), + Type.Object({ + prop1: Type.String(), + prop2: Type.String(), + prop7: Type.String(), + prop8: Type.String(), + }), + ]), + ]) + + Assert.IsEqual( + Value.Cast(A, { + prop1: '', + prop2: '', + prop7: '', + }), + { + prop1: '', + prop2: '', + prop7: '', + prop8: '', + }, + ) + }) + + it('should correctly score nested union types #4', () => { + const A = Type.Union([ + Type.Object({ + prop1: Type.String(), + prop2: Type.String(), + prop3: Type.String(), + }), + Type.Union([ + Type.Object({ + prop4: Type.String(), + prop5: Type.String(), + prop6: Type.String(), + }), + Type.Union([ + Type.Object({ + prop1: Type.String(), + prop2: Type.String(), + prop7: Type.String(), + prop8: Type.String(), + }), + Type.Union([ + Type.Object({ + prop1: Type.String(), + prop2: Type.String(), + prop9: Type.String(), + prop10: Type.String(), + }), + Type.Object({ + prop1: Type.String(), + prop2: Type.String(), + prop11: Type.String(), + prop12: Type.String(), + }), + ]), + ]), + ]), + ]) + + Assert.IsEqual( + Value.Cast(A, { + prop1: '', + prop2: '', + prop9: '', + }), + { + prop1: '', + prop2: '', + prop9: '', + prop10: '', + }, + ) + }) + + // ------------------------------------------------------------------------ + // ref: https://github.com/sinclairzx81/typebox/issues/1292 + // ------------------------------------------------------------------------ + it('should correctly score object unions with shared properties #1', () => { + const schema = Type.Union([ + Type.Object({ + summary: Type.Optional(Type.String()), + description: Type.Optional(Type.String()), + parameters: Type.Optional(Type.Array(Type.Any())), + responses: Type.Optional(Type.Record(Type.String(), Type.Any())), + requestBody: Type.Optional(Type.Any()), + }), + Type.Object({ + $ref: Type.String(), + summary: Type.Optional(Type.String()), + }), + ]) + + Assert.IsEqual( + Value.Cast(schema, { + summary: 'Test Summary', + parameters: {}, + }), + { + summary: 'Test Summary', + parameters: [], + }, + ) + }) + + it('should correctly score object unions with shared properties #2', () => { + const A = Type.Union([ + Type.Object({ + prop1: Type.String(), + prop2: Type.String(), + prop3: Type.String(), + }), + Type.Object({ + prop1: Type.String(), + prop2: Type.String(), + prop4: Type.String(), + prop5: Type.String(), + prop6: Type.String(), + prop7: Type.String(), + prop8: Type.String(), + prop9: Type.String(), + prop10: Type.String(), + }), + ]) + + Assert.IsEqual( + Value.Cast(A, { + prop1: '', + prop2: '', + prop7: '', + }), + { + prop1: '', + prop2: '', + prop4: '', + prop5: '', + prop6: '', + prop7: '', + prop8: '', + prop9: '', + prop10: '', + }, + ) + }) +}) diff --git a/test/runtime/value/cast/unknown.ts b/test/runtime/value/cast/unknown.ts new file mode 100644 index 000000000..10058ef2e --- /dev/null +++ b/test/runtime/value/cast/unknown.ts @@ -0,0 +1,52 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/cast/Unknown', () => { + const T = Type.Unknown() + it('Should upcast from string', () => { + const value = 'hello' + const result = Value.Cast(T, value) + Assert.IsEqual(result, value) + }) + it('Should upcast from number', () => { + const value = 1 + const result = Value.Cast(T, value) + Assert.IsEqual(result, value) + }) + it('Should upcast from boolean', () => { + const value = false + const result = Value.Cast(T, value) + Assert.IsEqual(result, value) + }) + it('Should upcast from object', () => { + const value = {} + const result = Value.Cast(T, value) + Assert.IsEqual(result, value) + }) + it('Should upcast from array', () => { + const value = [1] + const result = Value.Cast(T, value) + Assert.IsEqual(result, value) + }) + it('Should upcast from undefined', () => { + const value = undefined + const result = Value.Cast(T, value) + Assert.IsEqual(result, value) + }) + it('Should upcast from null', () => { + const value = null + const result = Value.Cast(T, value) + Assert.IsEqual(result, value) + }) + it('Should upcast from date', () => { + const value = new Date(100) + const result: any = Value.Cast(T, value) + Assert.IsEqual(result.getTime(), 100) + }) + it('Should preserve', () => { + const value = { a: 1, b: 2 } + const result = Value.Cast(T, value) + Assert.IsEqual(result, { a: 1, b: 2 }) + }) +}) diff --git a/test/runtime/value/cast/void.ts b/test/runtime/value/cast/void.ts new file mode 100644 index 000000000..3a0dd6f4f --- /dev/null +++ b/test/runtime/value/cast/void.ts @@ -0,0 +1,53 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/cast/Void', () => { + const T = Type.Void() + const E = undefined + it('Should upcast from string', () => { + const value = 'world' + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from number', () => { + const value = 1 + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from boolean', () => { + const value = true + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from object', () => { + const value = {} + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from array', () => { + const value = [1] + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from undefined', () => { + const value = undefined + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from null', () => { + const value = null + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should upcast from date', () => { + const value = new Date(100) + const result = Value.Cast(T, value) + Assert.IsEqual(result, E) + }) + it('Should preserve', () => { + const value = undefined + const result = Value.Cast(T, value) + Assert.IsEqual(result, undefined) + }) +}) diff --git a/test/runtime/value/check/any.ts b/test/runtime/value/check/any.ts new file mode 100644 index 000000000..302412c36 --- /dev/null +++ b/test/runtime/value/check/any.ts @@ -0,0 +1,47 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/check/Any', () => { + const T = Type.Any() + it('Should pass string', () => { + const value = 'hello' + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + it('Should pass number', () => { + const value = 1 + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + it('Should pass boolean', () => { + const value = true + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + it('Should pass null', () => { + const value = null + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + it('Should pass undefined', () => { + const value = undefined + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + it('Should pass object', () => { + const value = { a: 1 } + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + it('Should pass array', () => { + const value = [1, 2] + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + it('Should pass Date', () => { + const value = new Date() + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) +}) diff --git a/test/runtime/value/check/argument.ts b/test/runtime/value/check/argument.ts new file mode 100644 index 000000000..7a49e8bbf --- /dev/null +++ b/test/runtime/value/check/argument.ts @@ -0,0 +1,47 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/check/Argument', () => { + const T = Type.Argument(0) + it('Should pass string', () => { + const value = 'hello' + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + it('Should pass number', () => { + const value = 1 + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + it('Should pass boolean', () => { + const value = true + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + it('Should pass null', () => { + const value = null + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + it('Should pass undefined', () => { + const value = undefined + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + it('Should pass object', () => { + const value = { a: 1 } + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + it('Should pass array', () => { + const value = [1, 2] + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + it('Should pass Date', () => { + const value = new Date() + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) +}) diff --git a/test/runtime/value/check/array.ts b/test/runtime/value/check/array.ts new file mode 100644 index 000000000..9ad1be58c --- /dev/null +++ b/test/runtime/value/check/array.ts @@ -0,0 +1,155 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/check/Array', () => { + it('Should pass number array', () => { + const T = Type.Array(Type.Number()) + const value = [1, 2, 3] + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + it('Should fail number array', () => { + const T = Type.Array(Type.Number()) + const value = ['a', 'b', 'c'] + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should pass object array', () => { + const T = Type.Array(Type.Object({ x: Type.Number() })) + const value = [{ x: 1 }, { x: 1 }, { x: 1 }] + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + it('Should fail object array', () => { + const T = Type.Array(Type.Object({ x: Type.Number() })) + const value = [{ x: 1 }, { x: 1 }, 1] + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail Date', () => { + const value = new Date() + const result = Value.Check(Type.Array(Type.Any()), value) + Assert.IsEqual(result, false) + }) + it('Should validate array with unique primitive items', () => { + const T = Type.Array(Type.Number(), { uniqueItems: true }) + const result = Value.Check(T, [0, 1, 2]) + Assert.IsEqual(result, true) + }) + it('Should not validate array with non-unique primitive items', () => { + const T = Type.Array(Type.Number(), { uniqueItems: true }) + const result = Value.Check(T, [0, 0, 2]) + Assert.IsEqual(result, false) + }) + it('Should validate array with unique object items', () => { + const T = Type.Array(Type.Object({ x: Type.Number(), y: Type.Number() }), { uniqueItems: true }) + const result = Value.Check(T, [ + { x: 1, y: 1 }, + { x: 2, y: 2 }, + { x: 3, y: 3 }, + ]) + Assert.IsEqual(result, true) + }) + it('Should not validate array with non-unique object items', () => { + const T = Type.Array(Type.Object({ x: Type.Number(), y: Type.Number() }), { uniqueItems: true }) + const result = Value.Check(T, [ + { x: 1, y: 1 }, + { x: 1, y: 1 }, + { x: 3, y: 3 }, + ]) + Assert.IsEqual(result, false) + }) + // --------------------------------------------------------- + // Contains + // --------------------------------------------------------- + it('Should validate for contains', () => { + const T = Type.Array(Type.Number(), { contains: Type.Literal(1) }) + Assert.IsTrue(Value.Check(T, [1])) + Assert.IsTrue(Value.Check(T, [1, 2])) + Assert.IsFalse(Value.Check(T, [])) + Assert.IsFalse(Value.Check(T, [2])) + }) + it('Should validate for minContains', () => { + const T = Type.Array(Type.Number(), { contains: Type.Literal(1), minContains: 3 }) + Assert.IsTrue(Value.Check(T, [1, 1, 1, 2])) + Assert.IsTrue(Value.Check(T, [2, 1, 1, 1, 2])) + Assert.IsTrue(Value.Check(T, [1, 1, 1])) + Assert.IsFalse(Value.Check(T, [])) + Assert.IsFalse(Value.Check(T, [1, 1])) + Assert.IsFalse(Value.Check(T, [2])) + }) + it('Should validate for maxContains', () => { + const T = Type.Array(Type.Number(), { contains: Type.Literal(1), maxContains: 3 }) + Assert.IsTrue(Value.Check(T, [1, 1, 1])) + Assert.IsTrue(Value.Check(T, [1, 1])) + Assert.IsTrue(Value.Check(T, [2, 2, 2, 2, 1, 1, 1])) + Assert.IsFalse(Value.Check(T, [1, 1, 1, 1])) + }) + it('Should validate for minContains and maxContains', () => { + const T = Type.Array(Type.Number(), { contains: Type.Literal(1), minContains: 3, maxContains: 5 }) + Assert.IsFalse(Value.Check(T, [1, 1])) + Assert.IsTrue(Value.Check(T, [1, 1, 1])) + Assert.IsTrue(Value.Check(T, [1, 1, 1, 1])) + Assert.IsTrue(Value.Check(T, [1, 1, 1, 1, 1])) + Assert.IsFalse(Value.Check(T, [1, 1, 1, 1, 1, 1])) + }) + it('Should not validate minContains and maxContains when contains is unspecified', () => { + const T = Type.Array(Type.Number(), { minContains: 3, maxContains: 5 }) + Assert.IsFalse(Value.Check(T, [1, 1])) + Assert.IsFalse(Value.Check(T, [1, 1, 1])) + Assert.IsFalse(Value.Check(T, [1, 1, 1, 1])) + Assert.IsFalse(Value.Check(T, [1, 1, 1, 1, 1])) + Assert.IsFalse(Value.Check(T, [1, 1, 1, 1, 1, 1])) + }) + it('Should produce illogical schema when contains is not sub type of items', () => { + const T = Type.Array(Type.Number(), { contains: Type.String(), minContains: 3, maxContains: 5 }) + Assert.IsFalse(Value.Check(T, [1, 1])) + Assert.IsFalse(Value.Check(T, [1, 1, 1])) + Assert.IsFalse(Value.Check(T, [1, 1, 1, 1])) + Assert.IsFalse(Value.Check(T, [1, 1, 1, 1, 1])) + Assert.IsFalse(Value.Check(T, [1, 1, 1, 1, 1, 1])) + }) + // ---------------------------------------------------------------- + // Issue: https://github.com/sinclairzx81/typebox/discussions/607 + // ---------------------------------------------------------------- + it('Should correctly handle undefined array properties', () => { + const Answer = Type.Object({ + text: Type.String(), + isCorrect: Type.Boolean(), + }) + const Question = Type.Object({ + text: Type.String(), + options: Type.Array(Answer, { + minContains: 1, + maxContains: 1, + contains: Type.Object({ + text: Type.String(), + isCorrect: Type.Literal(true), + }), + }), + }) + Assert.IsFalse(Value.Check(Question, { text: 'A' })) + Assert.IsFalse(Value.Check(Question, { text: 'A', options: [] })) + Assert.IsTrue(Value.Check(Question, { text: 'A', options: [{ text: 'A', isCorrect: true }] })) + Assert.IsTrue( + Value.Check(Question, { + text: 'A', + options: [ + { text: 'A', isCorrect: true }, + { text: 'B', isCorrect: false }, + ], + }), + ) + Assert.IsFalse(Value.Check(Question, { text: 'A', options: [{ text: 'A', isCorrect: false }] })) + Assert.IsFalse( + Value.Check(Question, { + text: 'A', + options: [ + { text: 'A', isCorrect: true }, + { text: 'B', isCorrect: true }, + ], + }), + ) + }) +}) diff --git a/test/runtime/value/check/async-iterator.ts b/test/runtime/value/check/async-iterator.ts new file mode 100644 index 000000000..881f68c1c --- /dev/null +++ b/test/runtime/value/check/async-iterator.ts @@ -0,0 +1,24 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/check/AsyncIterator', () => { + it('Should pass async iterator 1', () => { + async function* f() {} + const T = Type.AsyncIterator(Type.Any()) + const result = Value.Check(T, f()) + Assert.IsEqual(result, true) + }) + it('Should pass async iterator 2', () => { + const T = Type.AsyncIterator(Type.Any()) + const result = Value.Check(T, { + [Symbol.asyncIterator]: () => {}, + }) + Assert.IsEqual(result, true) + }) + it('Should pass async iterator', () => { + const T = Type.AsyncIterator(Type.Any()) + const result = Value.Check(T, {}) + Assert.IsEqual(result, false) + }) +}) diff --git a/test/runtime/value/check/bigint.ts b/test/runtime/value/check/bigint.ts new file mode 100644 index 000000000..b253926f5 --- /dev/null +++ b/test/runtime/value/check/bigint.ts @@ -0,0 +1,41 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/check/BigInt', () => { + const T = Type.BigInt() + it('Should not validate NaN', () => { + const T = Type.BigInt() + const result = Value.Check(T, NaN) + Assert.IsEqual(result, false) + }) + it('Should not validate +Infinity', () => { + const T = Type.BigInt() + const result = Value.Check(T, Infinity) + Assert.IsEqual(result, false) + }) + it('Should not validate -Infinity', () => { + const T = Type.BigInt() + const result = Value.Check(T, -Infinity) + Assert.IsEqual(result, false) + }) + it('Should fail integer', () => { + const value = 1 + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail integer', () => { + const value = 3.14 + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail Date', () => { + const value = new Date() + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should pass bigint', () => { + const result = Value.Check(T, BigInt(0)) + Assert.IsEqual(result, true) + }) +}) diff --git a/test/runtime/value/check/boolean.ts b/test/runtime/value/check/boolean.ts new file mode 100644 index 000000000..3eafcf878 --- /dev/null +++ b/test/runtime/value/check/boolean.ts @@ -0,0 +1,47 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/check/Boolean', () => { + const T = Type.Boolean() + it('Should fail string', () => { + const value = 'hello' + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail number', () => { + const value = 1 + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should pass boolean', () => { + const value = true + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + it('Should fail null', () => { + const value = null + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail undefined', () => { + const value = undefined + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail object', () => { + const value = { a: 1 } + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail array', () => { + const value = [1, 2] + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail Date', () => { + const value = new Date() + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) +}) diff --git a/test/runtime/value/check/composite.ts b/test/runtime/value/check/composite.ts new file mode 100644 index 000000000..7c12e76c8 --- /dev/null +++ b/test/runtime/value/check/composite.ts @@ -0,0 +1,77 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/check/Composite', () => { + const A = Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }) + const B = Type.Object({ + a: Type.String(), + b: Type.String(), + c: Type.String(), + }) + const T = Type.Composite([A, B]) + it('Should pass composite', () => { + const value = { + x: 1, + y: 1, + z: 1, + a: '1', + b: '1', + c: '1', + } + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + it('Should fail intersect with invalid property', () => { + const value = { + x: true, + y: 1, + z: 1, + a: '1', + b: '1', + c: '1', + } + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail intersect with missing property', () => { + const value = { + y: 1, + z: 1, + a: '1', + b: '1', + c: '1', + } + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail intersect with primitive value', () => { + const value = 1 + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should pass intersect with optional properties', () => { + const A = Type.Object({ + x: Type.Optional(Type.Number()), + y: Type.Optional(Type.Number()), + z: Type.Optional(Type.Number()), + }) + const B = Type.Object({ + a: Type.String(), + b: Type.String(), + c: Type.String(), + }) + const T = Type.Composite([A, B]) + const value = { + a: '1', + b: '1', + c: '1', + } + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) +}) diff --git a/test/runtime/value/check/const.ts b/test/runtime/value/check/const.ts new file mode 100644 index 000000000..4b3db9f44 --- /dev/null +++ b/test/runtime/value/check/const.ts @@ -0,0 +1,42 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/check/Const', () => { + it('Should validate 1', () => { + const T = Type.Const(1) + Assert.IsTrue(Value.Check(T, 1)) + }) + it('Should validate 2', () => { + const T = Type.Const('hello') + Assert.IsTrue(Value.Check(T, 'hello')) + }) + it('Should validate 3', () => { + const T = Type.Const(true) + Assert.IsTrue(Value.Check(T, true)) + }) + it('Should validate 4', () => { + const T = Type.Const({ x: 1, y: 2 }) + Assert.IsTrue(Value.Check(T, { x: 1, y: 2 })) + }) + it('Should validate 5', () => { + const T = Type.Const([1, 2, 3]) + Assert.IsTrue(Value.Check(T, [1, 2, 3])) + }) + it('Should validate 6', () => { + const T = Type.Const([1, true, 'hello']) + Assert.IsTrue(Value.Check(T, [1, true, 'hello'])) + }) + it('Should validate 7', () => { + const T = Type.Const({ + x: [1, 2, 3, 4], + y: { x: 1, y: 2, z: 3 }, + }) + Assert.IsTrue( + Value.Check(T, { + x: [1, 2, 3, 4], + y: { x: 1, y: 2, z: 3 }, + }), + ) + }) +}) diff --git a/test/runtime/value/check/constructor.ts b/test/runtime/value/check/constructor.ts new file mode 100644 index 000000000..ba553e3d6 --- /dev/null +++ b/test/runtime/value/check/constructor.ts @@ -0,0 +1,87 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/check/Constructor', () => { + it('Should validate constructor 1', () => { + const T = Type.Constructor([], Type.Object({})) + Assert.IsTrue(Value.Check(T, class {})) + }) + it('Should validate constructor 2', () => { + const T = Type.Constructor([Type.Number()], Type.Object({})) + // note: constructor arguments are non-checkable + Assert.IsTrue(Value.Check(T, class {})) + }) + it('Should validate constructor 3', () => { + const T = Type.Constructor( + [Type.Number()], + Type.Object({ + method: Type.Function([], Type.Void()), + }), + ) + Assert.IsTrue( + Value.Check( + T, + class { + method() {} + }, + ), + ) + }) + it('Should validate constructor 4', () => { + const T = Type.Constructor( + [Type.Number()], + Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }), + ) + Assert.IsTrue( + Value.Check( + T, + class { + get x() { + return 1 + } + get y() { + return 1 + } + get z() { + return 1 + } + }, + ), + ) + }) + it('Should not validate constructor 1', () => { + const T = Type.Constructor([Type.Number()], Type.Object({})) + Assert.IsFalse(Value.Check(T, 1)) + }) + it('Should not validate constructor 2', () => { + const T = Type.Constructor( + [Type.Number()], + Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }), + ) + Assert.IsFalse( + Value.Check( + T, + class { + get x() { + return null + } + get y() { + return null + } + get z() { + return null + } + }, + ), + ) + }) +}) diff --git a/test/runtime/value/check/date.ts b/test/runtime/value/check/date.ts new file mode 100644 index 000000000..fd3cca859 --- /dev/null +++ b/test/runtime/value/check/date.ts @@ -0,0 +1,87 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/check/Date', () => { + const T = Type.Date() + it('Should fail string', () => { + const value = 'hello' + const result = Value.Check(T, value) + Assert.IsFalse(result) + }) + it('Should fail number', () => { + const value = 1 + const result = Value.Check(T, value) + Assert.IsFalse(result) + }) + it('Should fail boolean', () => { + const value = true + const result = Value.Check(T, value) + Assert.IsFalse(result) + }) + it('Should fail null', () => { + const value = null + const result = Value.Check(T, value) + Assert.IsFalse(result) + }) + it('Should fail undefined', () => { + const value = undefined + const result = Value.Check(T, value) + Assert.IsFalse(result) + }) + it('Should fail object', () => { + const value = { a: 1 } + const result = Value.Check(T, value) + Assert.IsFalse(result) + }) + it('Should fail array', () => { + const value = [1, 2] + const result = Value.Check(T, value) + Assert.IsFalse(result) + }) + it('Should pass Date', () => { + const value = new Date() + const result = Value.Check(T, value) + Assert.IsTrue(result) + }) + it('Should not validate Date if is invalid', () => { + const value = new Date('not-a-valid-date') + const result = Value.Check(T, value) + Assert.IsFalse(result) + }) + it('Should validate Date minimumTimestamp', () => { + const T = Type.Date({ minimumTimestamp: 10 }) + const R1 = Value.Check(T, new Date(9)) + const R2 = Value.Check(T, new Date(10)) + Assert.IsFalse(R1) + Assert.IsTrue(R2) + }) + it('Should validate Date maximumTimestamp', () => { + const T = Type.Date({ maximumTimestamp: 10 }) + const R1 = Value.Check(T, new Date(11)) + const R2 = Value.Check(T, new Date(10)) + Assert.IsFalse(R1) + Assert.IsTrue(R2) + }) + it('Should validate Date exclusiveMinimumTimestamp', () => { + const T = Type.Date({ exclusiveMinimumTimestamp: 10 }) + const R1 = Value.Check(T, new Date(10)) + const R2 = Value.Check(T, new Date(11)) + Assert.IsFalse(R1) + Assert.IsTrue(R2) + }) + it('Should validate Date exclusiveMaximumTimestamp', () => { + const T = Type.Date({ exclusiveMaximumTimestamp: 10 }) + const R1 = Value.Check(T, new Date(10)) + const R2 = Value.Check(T, new Date(9)) + Assert.IsFalse(R1) + Assert.IsTrue(R2) + }) + it('Should validate Date multipleOfTimestamp', () => { + const T = Type.Date({ multipleOfTimestamp: 2 }) + const R1 = Value.Check(T, new Date(1)) + const R2 = Value.Check(T, new Date(2)) + Assert.IsFalse(R1) + Assert.IsTrue(R2) + }) +}) diff --git a/test/runtime/value/check/enum.ts b/test/runtime/value/check/enum.ts new file mode 100644 index 000000000..34d9612cf --- /dev/null +++ b/test/runtime/value/check/enum.ts @@ -0,0 +1,31 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/check/Enum', () => { + enum Foo { + A = 1, + B = 2, + } + const T = Type.Enum(Foo) + it('Should pass enum option A', () => { + const value = Foo.A + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + it('Should pass enum option B', () => { + const value = Foo.A + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + it('Should fail unknown value', () => { + const value = 'unknown' + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail Date', () => { + const value = new Date() + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) +}) diff --git a/test/runtime/value/check/function.ts b/test/runtime/value/check/function.ts new file mode 100644 index 000000000..d7842f3c5 --- /dev/null +++ b/test/runtime/value/check/function.ts @@ -0,0 +1,28 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/check/Function', () => { + it('Should validate function 1', () => { + const T = Type.Function([Type.Number()], Type.Number()) + Assert.IsTrue(Value.Check(T, function () {})) + }) + it('Should validate function 2', () => { + const T = Type.Function([Type.Number()], Type.Number()) + // note: validation only checks typeof 'function' + Assert.IsTrue(Value.Check(T, function (a: string, b: string, c: string, d: string) {})) + }) + it('Should validate function 3', () => { + const T = Type.Function([Type.Number()], Type.Number()) + // note: validation only checks typeof 'function' + Assert.IsTrue( + Value.Check(T, function () { + return 'not-a-number' + }), + ) + }) + it('Should not validate function', () => { + const T = Type.Function([Type.Number()], Type.Number()) + Assert.IsFalse(Value.Check(T, 1)) + }) +}) diff --git a/test/runtime/value/check/index.ts b/test/runtime/value/check/index.ts new file mode 100644 index 000000000..ae71b7d46 --- /dev/null +++ b/test/runtime/value/check/index.ts @@ -0,0 +1,37 @@ +import './any' +import './argument' +import './array' +import './async-iterator' +import './bigint' +import './boolean' +import './composite' +import './const' +import './constructor' +import './date' +import './enum' +import './function' +import './integer' +import './intersect' +import './iterator' +import './keyof' +import './kind' +import './literal' +import './module' +import './never' +import './not' +import './null' +import './number' +import './object' +import './recursive' +import './ref' +import './record' +import './regexp' +import './string' +import './symbol' +import './template-literal' +import './tuple' +import './uint8array' +import './undefined' +import './union' +import './unknown' +import './void' diff --git a/test/runtime/value/check/integer.ts b/test/runtime/value/check/integer.ts new file mode 100644 index 000000000..9344a2937 --- /dev/null +++ b/test/runtime/value/check/integer.ts @@ -0,0 +1,37 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/check/Integer', () => { + const T = Type.Integer() + it('Should not validate NaN', () => { + const T = Type.Integer() + const result = Value.Check(T, NaN) + Assert.IsEqual(result, false) + }) + it('Should not validate +Infinity', () => { + const T = Type.Integer() + const result = Value.Check(T, Infinity) + Assert.IsEqual(result, false) + }) + it('Should not validate -Infinity', () => { + const T = Type.Integer() + const result = Value.Check(T, -Infinity) + Assert.IsEqual(result, false) + }) + it('Should pass integer', () => { + const value = 1 + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + it('Should fail integer', () => { + const value = 3.14 + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail Date', () => { + const value = new Date() + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) +}) diff --git a/test/runtime/value/check/intersect.ts b/test/runtime/value/check/intersect.ts new file mode 100644 index 000000000..e727f2bdb --- /dev/null +++ b/test/runtime/value/check/intersect.ts @@ -0,0 +1,219 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/check/Intersect', () => { + it('Should intersect number and number', () => { + const A = Type.Number() + const B = Type.Number() + const T = Type.Intersect([A, B], {}) + Assert.IsEqual(Value.Check(T, 1), true) + }) + it('Should not intersect string and number', () => { + const A = Type.String() + const B = Type.Number() + const T = Type.Intersect([A, B], {}) + Assert.IsEqual(Value.Check(T, 1), false) + Assert.IsEqual(Value.Check(T, '1'), false) + }) + it('Should intersect two objects', () => { + const A = Type.Object({ x: Type.Number() }) + const B = Type.Object({ y: Type.Number() }) + const T = Type.Intersect([A, B], {}) + Assert.IsEqual(Value.Check(T, { x: 1, y: 1 }), true) + }) + it('Should not intersect two objects with internal additionalProperties false', () => { + const A = Type.Object({ x: Type.Number() }, { additionalProperties: false }) + const B = Type.Object({ y: Type.Number() }, { additionalProperties: false }) + const T = Type.Intersect([A, B], {}) + Assert.IsEqual(Value.Check(T, { x: 1, y: 1 }), false) + }) + it('Should intersect two objects and mandate required properties', () => { + const A = Type.Object({ x: Type.Number() }) + const B = Type.Object({ y: Type.Number() }) + const T = Type.Intersect([A, B], {}) + Assert.IsEqual(Value.Check(T, { x: 1, y: 1 }), true) + Assert.IsEqual(Value.Check(T, { x: 1 }), false) + Assert.IsEqual(Value.Check(T, { x: 1 }), false) + }) + it('Should intersect two objects with unevaluated properties', () => { + const A = Type.Object({ x: Type.Number() }) + const B = Type.Object({ y: Type.Number() }) + const T = Type.Intersect([A, B], {}) + Assert.IsEqual(Value.Check(T, { x: 1, y: 1, z: 1 }), true) + }) + it('Should intersect two objects and restrict unevaluated properties', () => { + const A = Type.Object({ x: Type.Number() }) + const B = Type.Object({ y: Type.Number() }) + const T = Type.Intersect([A, B], { unevaluatedProperties: false }) + Assert.IsEqual(Value.Check(T, { x: 1, y: 1, z: 1 }), false) + }) + it('Should intersect two objects and allow unevaluated properties of number', () => { + const A = Type.Object({ x: Type.Number() }) + const B = Type.Object({ y: Type.Number() }) + const T = Type.Intersect([A, B], { unevaluatedProperties: Type.Number() }) + Assert.IsEqual(Value.Check(T, { x: 1, y: 2, z: 3 }), true) + Assert.IsEqual(Value.Check(T, { x: 1, y: 2, z: '1' }), false) + }) + it('Should intersect two union objects with overlapping properties of the same type', () => { + const A = Type.Union([Type.Object({ x: Type.Number() })]) + const B = Type.Union([Type.Object({ x: Type.Number() })]) + const T = Type.Intersect([A, B]) + Assert.IsEqual(Value.Check(T, { x: 1 }), true) + Assert.IsEqual(Value.Check(T, { x: '1' }), false) + }) + it('Should not intersect two union objects with overlapping properties of varying types', () => { + const A = Type.Union([Type.Object({ x: Type.Number() })]) + const B = Type.Union([Type.Object({ x: Type.String() })]) + const T = Type.Intersect([A, B]) + Assert.IsEqual(Value.Check(T, { x: 1 }), false) + Assert.IsEqual(Value.Check(T, { x: '1' }), false) + }) + it('Should intersect two union objects with non-overlapping properties', () => { + const A = Type.Union([Type.Object({ x: Type.Number() })]) + const B = Type.Union([Type.Object({ y: Type.Number() })]) + const T = Type.Intersect([A, B]) + Assert.IsEqual(Value.Check(T, { x: 1, y: 1 }), true) + }) + it('Should not intersect two union objects with non-overlapping properties for additionalProperties false', () => { + const A = Type.Union([Type.Object({ x: Type.Number() }, { additionalProperties: false })]) + const B = Type.Union([Type.Object({ y: Type.Number() }, { additionalProperties: false })]) + const T = Type.Intersect([A, B]) + Assert.IsEqual(Value.Check(T, { x: 1, y: 1 }), false) + }) + it('unevaluatedProperties with Record 1', () => { + const T = Type.Intersect( + [ + Type.Record(Type.Number(), Type.String()), + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + ], + { + unevaluatedProperties: false, + }, + ) + Assert.IsEqual(Value.Check(T, { x: 1, y: 2 }), true) + }) + it('unevaluatedProperties with Record 2', () => { + const T = Type.Intersect( + [ + Type.Record(Type.Number(), Type.String()), + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + ], + { + unevaluatedProperties: false, + }, + ) + Assert.IsEqual(Value.Check(T, { x: 1, y: 2, 0: 'hello' }), true) + }) + it('unevaluatedProperties with Record 3', () => { + const T = Type.Intersect( + [ + Type.Record(Type.Number(), Type.String()), + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + ], + { + unevaluatedProperties: false, + }, + ) + Assert.IsEqual(Value.Check(T, { x: 1, y: 2, 0: 1 }), false) + }) + it('unevaluatedProperties with Record 4', () => { + const T = Type.Intersect( + [ + Type.Record(Type.Number(), Type.String()), + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + ], + { + unevaluatedProperties: Type.Boolean(), + }, + ) + Assert.IsEqual(Value.Check(T, { x: 1, y: 2 }), true) + }) + it('unevaluatedProperties with Record 5', () => { + const T = Type.Intersect( + [ + Type.Record(Type.Number(), Type.String()), + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + ], + { + unevaluatedProperties: Type.Boolean(), + }, + ) + Assert.IsEqual(Value.Check(T, { x: 1, y: 2, z: true }), true) + }) + it('unevaluatedProperties with Record 6', () => { + const T = Type.Intersect( + [ + Type.Record(Type.Number(), Type.String()), + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + ], + { + unevaluatedProperties: Type.Boolean(), + }, + ) + Assert.IsEqual(Value.Check(T, { x: 1, y: 2, z: 1 }), false) + }) + it('unevaluatedProperties with Record 7', () => { + const T = Type.Intersect( + [ + Type.Record(Type.Number(), Type.String()), + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + ], + { + unevaluatedProperties: Type.Boolean(), + }, + ) + Assert.IsEqual(Value.Check(T, { x: 1, y: 2, 0: '' }), true) + }) + it('unevaluatedProperties with Record 8', () => { + const T = Type.Intersect( + [ + Type.Record(Type.Number(), Type.String()), + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + ], + { + unevaluatedProperties: Type.Boolean(), + }, + ) + Assert.IsEqual(Value.Check(T, { x: 1, y: 2, 0: '', z: true }), true) + }) + it('unevaluatedProperties with Record 9', () => { + const T = Type.Intersect( + [ + Type.Record(Type.Number(), Type.String()), + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + ], + { + unevaluatedProperties: Type.Boolean(), + }, + ) + Assert.IsEqual(Value.Check(T, { x: 1, y: 2, 0: '', z: 1 }), false) + }) +}) diff --git a/test/runtime/value/check/iterator.ts b/test/runtime/value/check/iterator.ts new file mode 100644 index 000000000..69274d107 --- /dev/null +++ b/test/runtime/value/check/iterator.ts @@ -0,0 +1,24 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/check/Iterator', () => { + it('Should pass iterator 1', () => { + function* f() {} + const T = Type.Iterator(Type.Any()) + const result = Value.Check(T, f()) + Assert.IsEqual(result, true) + }) + it('Should pass iterator 2', () => { + const T = Type.Iterator(Type.Any()) + const result = Value.Check(T, { + [Symbol.iterator]: () => {}, + }) + Assert.IsEqual(result, true) + }) + it('Should pass iterator', () => { + const T = Type.Iterator(Type.Any()) + const result = Value.Check(T, {}) + Assert.IsEqual(result, false) + }) +}) diff --git a/test/runtime/value/check/keyof.ts b/test/runtime/value/check/keyof.ts new file mode 100644 index 000000000..a3407c95d --- /dev/null +++ b/test/runtime/value/check/keyof.ts @@ -0,0 +1,33 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/check/KeyOf', () => { + const T = Type.KeyOf( + Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }), + ) + it('Should pass keyof', () => { + const value = 'x' + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + it('Should fail keyof', () => { + const value = 'w' + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail keyof with undefined', () => { + const value = undefined + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail keyof with null', () => { + const value = null + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) +}) diff --git a/test/runtime/value/check/kind.ts b/test/runtime/value/check/kind.ts new file mode 100644 index 000000000..9a16eed65 --- /dev/null +++ b/test/runtime/value/check/kind.ts @@ -0,0 +1,107 @@ +import { Value } from '@sinclair/typebox/value' +import { TypeRegistry, Type, Kind, TSchema } from '@sinclair/typebox' +import { Assert } from '../../assert' + +describe('value/check/Kind', () => { + // ------------------------------------------------------------ + // Fixtures + // ------------------------------------------------------------ + beforeEach(() => TypeRegistry.Set('PI', (_, value) => value === Math.PI)) + afterEach(() => TypeRegistry.Delete('PI')) + // ------------------------------------------------------------ + // Tests + // ------------------------------------------------------------ + it('Should validate', () => { + const T = Type.Unsafe({ [Kind]: 'PI' }) + Assert.IsTrue(Value.Check(T, Math.PI)) + }) + it('Should not validate', () => { + const T = Type.Unsafe({ [Kind]: 'PI' }) + Assert.IsFalse(Value.Check(T, Math.PI * 2)) + }) + it('Should validate in object', () => { + const T = Type.Object({ + x: Type.Unsafe({ [Kind]: 'PI' }), + }) + Assert.IsTrue(Value.Check(T, { x: Math.PI })) + }) + it('Should not validate in object', () => { + const T = Type.Object({ + x: Type.Unsafe({ [Kind]: 'PI' }), + }) + Assert.IsFalse(Value.Check(T, { x: Math.PI * 2 })) + }) + it('Should validate in array', () => { + const T = Type.Array(Type.Unsafe({ [Kind]: 'PI' })) + Assert.IsTrue(Value.Check(T, [Math.PI])) + }) + it('Should not validate in array', () => { + const T = Type.Array(Type.Unsafe({ [Kind]: 'PI' })) + Assert.IsFalse(Value.Check(T, [Math.PI * 2])) + }) + it('Should validate in tuple', () => { + const T = Type.Tuple([Type.Unsafe({ [Kind]: 'PI' })]) + Assert.IsTrue(Value.Check(T, [Math.PI])) + }) + it('Should not validate in tuple', () => { + const T = Type.Tuple([Type.Unsafe({ [Kind]: 'PI' })]) + Assert.IsFalse(Value.Check(T, [Math.PI * 2])) + }) + // ------------------------------------------------------------ + // Instances + // ------------------------------------------------------------ + it('Should receive kind instance on registry callback', () => { + const stack: string[] = [] + TypeRegistry.Set('Kind', (schema: unknown) => { + // prettier-ignore + return (typeof schema === 'object' && schema !== null && Kind in schema && schema[Kind] === 'Kind' && '$id' in schema && typeof schema.$id === 'string') + ? (() => { stack.push(schema.$id); return true })() + : false + }) + const A = { [Kind]: 'Kind', $id: 'A' } as TSchema + const B = { [Kind]: 'Kind', $id: 'B' } as TSchema + const T = Type.Object({ a: A, b: B }) + const R = Value.Check(T, { a: null, b: null }) + Assert.IsTrue(R) + Assert.IsEqual(stack[0], 'A') + Assert.IsEqual(stack[1], 'B') + TypeRegistry.Delete('Kind') + }) + it('Should retain kind instances on subsequent check', () => { + let stack: string[] = [] + TypeRegistry.Set('Kind', (schema: unknown) => { + // prettier-ignore + return (typeof schema === 'object' && schema !== null && Kind in schema && schema[Kind] === 'Kind' && '$id' in schema && typeof schema.$id === 'string') + ? (() => { stack.push(schema.$id); return true })() + : false + }) + const A = { [Kind]: 'Kind', $id: 'A' } as TSchema + const B = { [Kind]: 'Kind', $id: 'B' } as TSchema + const C = { [Kind]: 'Kind', $id: 'C' } as TSchema + const D = { [Kind]: 'Kind', $id: 'D' } as TSchema + const T1 = Type.Object({ a: A, b: B }) + const T2 = Type.Object({ a: C, b: D }) + // run T1 check + const R2 = Value.Check(T1, { a: null, b: null }) + Assert.IsTrue(R2) + Assert.IsEqual(stack.length, 2) + Assert.IsEqual(stack[0], 'A') + Assert.IsEqual(stack[1], 'B') + stack = [] + // run T2 check + const R3 = Value.Check(T2, { a: null, b: null }) + Assert.IsTrue(R3) + Assert.IsEqual(stack.length, 2) + Assert.IsEqual(stack[0], 'C') + Assert.IsEqual(stack[1], 'D') + stack = [] + // run T1 check + const R4 = Value.Check(T1, { a: null, b: null }) + Assert.IsTrue(R4) + Assert.IsEqual(stack.length, 2) + Assert.IsEqual(stack[0], 'A') + Assert.IsEqual(stack[1], 'B') + stack = [] + TypeRegistry.Delete('Kind') + }) +}) diff --git a/test/runtime/value/check/literal.ts b/test/runtime/value/check/literal.ts new file mode 100644 index 000000000..4acca4557 --- /dev/null +++ b/test/runtime/value/check/literal.ts @@ -0,0 +1,27 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/check/Literal', () => { + const T = Type.Literal('hello') + it('Should pass literal', () => { + const value = 'hello' + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + it('Should fail literal', () => { + const value = 1 + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail literal with undefined', () => { + const value = undefined + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail literal with null', () => { + const value = null + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) +}) diff --git a/test/runtime/value/check/module.ts b/test/runtime/value/check/module.ts new file mode 100644 index 000000000..af80b0e68 --- /dev/null +++ b/test/runtime/value/check/module.ts @@ -0,0 +1,146 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert' + +describe('value/check/Module', () => { + it('Should validate string', () => { + const Module = Type.Module({ + A: Type.String(), + }) + const T = Module.Import('A') + Assert.IsTrue(Value.Check(T, 'hello')) + Assert.IsFalse(Value.Check(T, true)) + }) + it('Should validate referenced string', () => { + const Module = Type.Module({ + A: Type.String(), + B: Type.Ref('A'), + }) + const T = Module.Import('B') + Assert.IsTrue(Value.Check(T, 'hello')) + Assert.IsFalse(Value.Check(T, true)) + }) + it('Should validate self referential', () => { + const Module = Type.Module({ + A: Type.Object({ + nodes: Type.Array(Type.Ref('A')), + }), + }) + const T = Module.Import('A') + Assert.IsTrue(Value.Check(T, { nodes: [{ nodes: [{ nodes: [] }, { nodes: [] }] }] })) + Assert.IsFalse(Value.Check(T, { nodes: [{ nodes: [{ nodes: [] }, { nodes: false }] }] })) + Assert.IsFalse(Value.Check(T, true)) + }) + it('Should validate mutual recursive', () => { + const Module = Type.Module({ + A: Type.Object({ + b: Type.Ref('B'), + }), + B: Type.Object({ + a: Type.Union([Type.Ref('A'), Type.Null()]), + }), + }) + const T = Module.Import('A') + Assert.IsTrue(Value.Check(T, { b: { a: null } })) + Assert.IsTrue(Value.Check(T, { b: { a: { b: { a: null } } } })) + + Assert.IsFalse(Value.Check(T, { b: { a: 1 } })) + Assert.IsFalse(Value.Check(T, { b: { a: { b: { a: 1 } } } })) + Assert.IsFalse(Value.Check(T, true)) + }) + it('Should validate mutual recursive (Array)', () => { + const Module = Type.Module({ + A: Type.Object({ + b: Type.Ref('B'), + }), + B: Type.Object({ + a: Type.Array(Type.Ref('A')), + }), + }) + const T = Module.Import('A') + Assert.IsTrue(Value.Check(T, { b: { a: [{ b: { a: [] } }] } })) + Assert.IsFalse(Value.Check(T, { b: { a: [{ b: { a: [null] } }] } })) + Assert.IsFalse(Value.Check(T, true)) + }) + it('Should validate deep referential', () => { + const Module = Type.Module({ + A: Type.Ref('B'), + B: Type.Ref('C'), + C: Type.Ref('D'), + D: Type.Ref('E'), + E: Type.Ref('F'), + F: Type.Ref('G'), + G: Type.Ref('H'), + H: Type.Literal('hello'), + }) + const T = Module.Import('A') + Assert.IsTrue(Value.Check(T, 'hello')) + Assert.IsFalse(Value.Check(T, 'world')) + }) + // ---------------------------------------------------------------- + // Modifiers + // ---------------------------------------------------------------- + it('Should validate objects with property modifiers 1', () => { + const Module = Type.Module({ + T: Type.Object({ + x: Type.ReadonlyOptional(Type.Null()), + y: Type.Readonly(Type.Null()), + z: Type.Optional(Type.Null()), + w: Type.Null(), + }), + }) + const T = Module.Import('T') + Assert.IsTrue(Value.Check(T, { x: null, y: null, w: null })) + Assert.IsTrue(Value.Check(T, { y: null, w: null })) + Assert.IsFalse(Value.Check(T, { x: 1, y: null, w: null })) + }) + it('Should validate objects with property modifiers 2', () => { + const Module = Type.Module({ + T: Type.Object({ + x: Type.ReadonlyOptional(Type.Array(Type.Null())), + y: Type.Readonly(Type.Array(Type.Null())), + z: Type.Optional(Type.Array(Type.Null())), + w: Type.Array(Type.Null()), + }), + }) + const T = Module.Import('T') + Assert.IsTrue(Value.Check(T, { x: [null], y: [null], w: [null] })) + Assert.IsTrue(Value.Check(T, { y: [null], w: [null] })) + Assert.IsFalse(Value.Check(T, { x: [1], y: [null], w: [null] })) + }) + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/1109 + // ---------------------------------------------------------------- + it('Should validate deep referential 1', () => { + const Module = Type.Module({ + A: Type.Union([Type.Literal('Foo'), Type.Literal('Bar')]), + B: Type.Ref('A'), + C: Type.Object({ ref: Type.Ref('B') }), + D: Type.Union([Type.Ref('B'), Type.Ref('C')]), + }) + Assert.IsTrue(Value.Check(Module.Import('A') as never, 'Foo')) + Assert.IsTrue(Value.Check(Module.Import('A') as never, 'Bar')) + Assert.IsTrue(Value.Check(Module.Import('B') as never, 'Foo')) + Assert.IsTrue(Value.Check(Module.Import('B') as never, 'Bar')) + Assert.IsTrue(Value.Check(Module.Import('C') as never, { ref: 'Foo' })) + Assert.IsTrue(Value.Check(Module.Import('C') as never, { ref: 'Bar' })) + Assert.IsTrue(Value.Check(Module.Import('D') as never, 'Foo')) + Assert.IsTrue(Value.Check(Module.Import('D') as never, 'Bar')) + Assert.IsTrue(Value.Check(Module.Import('D') as never, { ref: 'Foo' })) + Assert.IsTrue(Value.Check(Module.Import('D') as never, { ref: 'Bar' })) + }) + it('Should validate deep referential 2', () => { + const Module = Type.Module({ + A: Type.Literal('Foo'), + B: Type.Ref('A'), + C: Type.Ref('B'), + D: Type.Ref('C'), + E: Type.Ref('D'), + }) + Assert.IsTrue(Value.Check(Module.Import('A'), 'Foo')) + Assert.IsTrue(Value.Check(Module.Import('B'), 'Foo')) + Assert.IsTrue(Value.Check(Module.Import('C'), 'Foo')) + Assert.IsTrue(Value.Check(Module.Import('D'), 'Foo')) + Assert.IsTrue(Value.Check(Module.Import('E'), 'Foo')) + }) +}) diff --git a/test/runtime/value/check/never.ts b/test/runtime/value/check/never.ts new file mode 100644 index 000000000..325f189f3 --- /dev/null +++ b/test/runtime/value/check/never.ts @@ -0,0 +1,42 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/check/Never', () => { + const T = Type.Never() + it('Should fail string', () => { + const value = 'hello' + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail number', () => { + const value = 1 + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail boolean', () => { + const value = true + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail null', () => { + const value = null + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail undefined', () => { + const value = undefined + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail object', () => { + const value = { a: 1 } + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail array', () => { + const value = [1, 2] + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) +}) diff --git a/test/runtime/value/check/not.ts b/test/runtime/value/check/not.ts new file mode 100644 index 000000000..866ec401a --- /dev/null +++ b/test/runtime/value/check/not.ts @@ -0,0 +1,30 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/check/Not', () => { + it('Should validate with not number', () => { + const T = Type.Not(Type.Number()) + Assert.IsEqual(Value.Check(T, 1), false) + Assert.IsEqual(Value.Check(T, 'A'), true) + }) + it('Should validate with union left', () => { + // prettier-ignore + const T = Type.Not(Type.Union([ + Type.Literal('A'), + Type.Literal('B'), + Type.Literal('C') + ])) + Assert.IsEqual(Value.Check(T, 'A'), false) + Assert.IsEqual(Value.Check(T, 'B'), false) + Assert.IsEqual(Value.Check(T, 'C'), false) + Assert.IsEqual(Value.Check(T, 'D'), true) + }) + it('Should validate with union right', () => { + // prettier-ignore + const T = Type.Not(Type.Number()) + Assert.IsEqual(Value.Check(T, 1), false) + Assert.IsEqual(Value.Check(T, 'A'), true) + Assert.IsEqual(Value.Check(T, true), true) + }) +}) diff --git a/test/runtime/value/check/null.ts b/test/runtime/value/check/null.ts new file mode 100644 index 000000000..2a92ec855 --- /dev/null +++ b/test/runtime/value/check/null.ts @@ -0,0 +1,47 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/check/Null', () => { + const T = Type.Null() + it('Should fail string', () => { + const value = 'hello' + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail number', () => { + const value = 1 + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail boolean', () => { + const value = true + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should pass null', () => { + const value = null + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + it('Should fail undefined', () => { + const value = undefined + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail object', () => { + const value = { a: 1 } + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail array', () => { + const value = [1, 2] + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail Date', () => { + const value = new Date() + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) +}) diff --git a/test/runtime/value/check/number.ts b/test/runtime/value/check/number.ts new file mode 100644 index 000000000..a38909989 --- /dev/null +++ b/test/runtime/value/check/number.ts @@ -0,0 +1,67 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/check/Number', () => { + const T = Type.Number() + it('Should not validate NaN', () => { + const T = Type.Number() + const result = Value.Check(T, NaN) + Assert.IsEqual(result, false) + }) + it('Should not validate +Infinity', () => { + const T = Type.Number() + const result = Value.Check(T, Infinity) + Assert.IsEqual(result, false) + }) + it('Should not validate -Infinity', () => { + const T = Type.Number() + const result = Value.Check(T, -Infinity) + Assert.IsEqual(result, false) + }) + it('Should fail string', () => { + const value = 'hello' + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should pass number', () => { + const value = 1 + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + it('Should fail boolean', () => { + const value = true + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail null', () => { + const value = null + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail undefined', () => { + const value = undefined + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail object', () => { + const value = { a: 1 } + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail array', () => { + const value = [1, 2] + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail Date', () => { + const value = new Date() + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + + it('Should fail NaN', () => { + const result = Value.Check(Type.Number(), NaN) + Assert.IsEqual(result, false) + }) +}) diff --git a/test/runtime/value/check/object.ts b/test/runtime/value/check/object.ts new file mode 100644 index 000000000..fe6b5b718 --- /dev/null +++ b/test/runtime/value/check/object.ts @@ -0,0 +1,212 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/check/Object', () => { + const T = Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + a: Type.String(), + b: Type.String(), + c: Type.String(), + }) + it('Should pass object', () => { + const value = { + x: 1, + y: 1, + z: 1, + a: '1', + b: '1', + c: '1', + } + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + it('Should fail object with additional properties', () => { + const T = Type.Object( + { + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }, + { additionalProperties: false }, + ) + const value = { + x: 1, + y: 1, + z: 1, + a: 1, + } + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail object with invalid property', () => { + const value = { + x: true, + y: 1, + z: 1, + a: '1', + b: '1', + c: '1', + } + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail object with missing property', () => { + const value = { + y: 1, + z: 1, + a: '1', + b: '1', + c: '1', + } + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should pass object with optional properties', () => { + const T = Type.Object({ + x: Type.Optional(Type.Number()), + y: Type.Optional(Type.Number()), + z: Type.Optional(Type.Number()), + a: Type.String(), + b: Type.String(), + c: Type.String(), + }) + const value = { + a: '1', + b: '1', + c: '1', + } + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + it('Should fail object with null', () => { + const value = null + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail object with undefined', () => { + const value = undefined + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should validate schema additional properties of string', () => { + const T = Type.Object( + { + x: Type.Number(), + y: Type.Number(), + }, + { + additionalProperties: Type.String(), + }, + ) + Assert.IsEqual( + Value.Check(T, { + x: 1, + y: 2, + z: 'hello', + }), + true, + ) + + Assert.IsEqual( + Value.Check(T, { + x: 1, + y: 2, + z: 3, + }), + false, + ) + }) + it('Should validate schema additional properties of array', () => { + const T = Type.Object( + { + x: Type.Number(), + y: Type.Number(), + }, + { + additionalProperties: Type.Array(Type.Number()), + }, + ) + Assert.IsEqual( + Value.Check(T, { + x: 1, + y: 2, + z: [0, 1, 2], + }), + true, + ) + Assert.IsEqual( + Value.Check(T, { + x: 1, + y: 2, + z: 3, + }), + false, + ) + }) + it('Should validate schema additional properties of object', () => { + const T = Type.Object( + { + x: Type.Number(), + y: Type.Number(), + }, + { + additionalProperties: Type.Object({ + z: Type.Number(), + }), + }, + ) + Assert.IsEqual( + Value.Check(T, { + x: 1, + y: 2, + z: { z: 1 }, + }), + true, + ) + Assert.IsEqual( + Value.Check(T, { + x: 1, + y: 2, + z: 3, + }), + false, + ) + }) + it('Should check for property key if property type is undefined', () => { + const T = Type.Object({ x: Type.Undefined() }) + Assert.IsEqual(Value.Check(T, { x: undefined }), true) + Assert.IsEqual(Value.Check(T, {}), false) + }) + it('Should check for property key if property type extends undefined', () => { + const T = Type.Object({ x: Type.Union([Type.Number(), Type.Undefined()]) }) + Assert.IsEqual(Value.Check(T, { x: 1 }), true) + Assert.IsEqual(Value.Check(T, { x: undefined }), true) + Assert.IsEqual(Value.Check(T, {}), false) + }) + it('Should not check for property key if property type is undefined and optional', () => { + const T = Type.Object({ x: Type.Optional(Type.Undefined()) }) + Assert.IsEqual(Value.Check(T, { x: undefined }), true) + Assert.IsEqual(Value.Check(T, {}), true) + }) + it('Should not check for property key if property type extends undefined and optional', () => { + const T = Type.Object({ x: Type.Optional(Type.Union([Type.Number(), Type.Undefined()])) }) + Assert.IsEqual(Value.Check(T, { x: 1 }), true) + Assert.IsEqual(Value.Check(T, { x: undefined }), true) + Assert.IsEqual(Value.Check(T, {}), true) + }) + it('Should check undefined for optional property of number', () => { + const T = Type.Object({ x: Type.Optional(Type.Number()) }) + Assert.IsEqual(Value.Check(T, { x: 1 }), true) + Assert.IsEqual(Value.Check(T, { x: undefined }), true) // allowed by default + Assert.IsEqual(Value.Check(T, {}), true) + }) + it('Should check undefined for optional property of undefined', () => { + const T = Type.Object({ x: Type.Optional(Type.Undefined()) }) + Assert.IsEqual(Value.Check(T, { x: 1 }), false) + Assert.IsEqual(Value.Check(T, {}), true) + Assert.IsEqual(Value.Check(T, { x: undefined }), true) + }) +}) diff --git a/test/runtime/value/check/record.ts b/test/runtime/value/check/record.ts new file mode 100644 index 000000000..c4b9c1fd8 --- /dev/null +++ b/test/runtime/value/check/record.ts @@ -0,0 +1,297 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/check/Record', () => { + it('Should pass record', () => { + const T = Type.Record( + Type.String(), + Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }), + ) + const value = { + position: { + x: 1, + y: 2, + z: 3, + }, + } + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + it('Should fail when below minProperties', () => { + const T = Type.Record(Type.String(), Type.Number(), { minProperties: 4 }) + Assert.IsEqual(Value.Check(T, { a: 1, b: 2, c: 3, d: 4 }), true) + Assert.IsEqual(Value.Check(T, { a: 1, b: 2, c: 3 }), false) + }) + it('Should fail when above maxProperties', () => { + const T = Type.Record(Type.String(), Type.Number(), { maxProperties: 4 }) + Assert.IsEqual(Value.Check(T, { a: 1, b: 2, c: 3, d: 4 }), true) + Assert.IsEqual(Value.Check(T, { a: 1, b: 2, c: 3, d: 4, e: 5 }), false) + }) + it('Should fail with illogical minProperties | maxProperties', () => { + const T = Type.Record(Type.String(), Type.Number(), { minProperties: 5, maxProperties: 4 }) + Assert.IsEqual(Value.Check(T, { a: 1, b: 2, c: 3 }), false) + Assert.IsEqual(Value.Check(T, { a: 1, b: 2, c: 3, d: 4 }), false) + Assert.IsEqual(Value.Check(T, { a: 1, b: 2, c: 3, d: 4, e: 5 }), false) + }) + it('Should fail record with Date', () => { + const T = Type.Record(Type.String(), Type.String()) + const result = Value.Check(T, new Date()) + Assert.IsEqual(result, false) + }) + it('Should fail record with Uint8Array', () => { + const T = Type.Record(Type.String(), Type.String()) + const result = Value.Check(T, new Uint8Array()) + Assert.IsEqual(result, false) + }) + it('Should fail record with missing property', () => { + const T = Type.Record( + Type.String(), + Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }), + ) + const value = { + position: { + x: 1, + y: 2, + }, + } + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail record with invalid property', () => { + const T = Type.Record( + Type.String(), + Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }), + ) + const value = { + position: { + x: 1, + y: 2, + z: '3', + }, + } + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should pass record with optional property', () => { + const T = Type.Record( + Type.String(), + Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Optional(Type.Number()), + }), + ) + const value = { + position: { + x: 1, + y: 2, + }, + } + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + it('Should pass record with optional property', () => { + const T = Type.Record( + Type.String(), + Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Optional(Type.Number()), + }), + ) + const value = { + position: { + x: 1, + y: 2, + }, + } + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + it('Should validate when specifying regular expressions', () => { + const K = Type.RegExp(/^op_.*$/) + const T = Type.Record(K, Type.Number()) + const R = Value.Check(T, { + op_a: 1, + op_b: 2, + op_c: 3, + }) + Assert.IsTrue(R) + }) + it('Should not validate when specifying regular expressions and passing invalid property', () => { + const K = Type.RegExp(/^op_.*$/) + const T = Type.Record(K, Type.Number(), { additionalProperties: false }) + const R = Value.Check(T, { + op_a: 1, + op_b: 2, + aop_c: 3, + }) + Assert.IsFalse(R) + }) + it('Should validate with quoted string pattern', () => { + const K = Type.String({ pattern: "'(a|b|c)" }) + const T = Type.Record(K, Type.Number()) + const R = Value.Check(T, { + "'a": 1, + "'b": 2, + "'c": 3, + }) + Assert.IsTrue(R) + }) + it('Should validate with forward-slash pattern', () => { + const K = Type.String({ pattern: '/(a|b|c)' }) + const T = Type.Record(K, Type.Number()) + const R = Value.Check(T, { + '/a': 1, + '/b': 2, + '/c': 3, + }) + Assert.IsTrue(R) + }) + // ------------------------------------------------- + // Number Key + // ------------------------------------------------- + it('Should pass record with number key', () => { + const T = Type.Record(Type.Number(), Type.String()) + const value = { + 0: 'a', + 1: 'a', + 2: 'a', + } + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + + it('Should not pass record with invalid number key', () => { + const T = Type.Record(Type.Number(), Type.String(), { additionalProperties: false }) + const value = { + a: 'a', + 1: 'a', + 2: 'a', + } + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + // ------------------------------------------------- + // Integer Key + // ------------------------------------------------- + it('Should pass record with integer key', () => { + const T = Type.Record(Type.Integer(), Type.String()) + const value = { + 0: 'a', + 1: 'a', + 2: 'a', + } + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + it('Should not pass record with invalid integer key', () => { + const T = Type.Record(Type.Integer(), Type.String(), { additionalProperties: false }) + const value = { + a: 'a', + 1: 'a', + 2: 'a', + } + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + // ------------------------------------------------------------ + // AdditionalProperties + // ------------------------------------------------------------ + it('AdditionalProperties 1', () => { + const T = Type.Record(Type.Number(), Type.String(), { additionalProperties: true }) + const R = Value.Check(T, { 1: '', 2: '', x: 1, y: 2, z: 3 }) + Assert.IsEqual(R, true) + }) + it('AdditionalProperties 2', () => { + const T = Type.Record(Type.Number(), Type.String(), { additionalProperties: false }) + const R = Value.Check(T, { 1: '', 2: '', 3: '' }) + Assert.IsEqual(R, true) + }) + it('AdditionalProperties 3', () => { + const T = Type.Record(Type.Number(), Type.String(), { additionalProperties: false }) + const R = Value.Check(T, { 1: '', 2: '', x: '' }) + Assert.IsEqual(R, false) + }) + it('AdditionalProperties 4', () => { + const T = Type.Record(Type.Number(), Type.String(), { additionalProperties: Type.Boolean() }) + const R = Value.Check(T, { 1: '', 2: '', x: '' }) + Assert.IsEqual(R, false) + }) + it('AdditionalProperties 5', () => { + const T = Type.Record(Type.Number(), Type.String(), { additionalProperties: Type.Boolean() }) + const R = Value.Check(T, { 1: '', 2: '', x: true }) + Assert.IsEqual(R, true) + }) + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/916 + // ---------------------------------------------------------------- + it('Should validate for string keys', () => { + const T = Type.Record(Type.String(), Type.Null(), { + additionalProperties: false, + }) + const R = Value.Check(T, { + a: null, + b: null, + 0: null, + 1: null, + }) + Assert.IsEqual(R, true) + }) + it('Should validate for number keys', () => { + const T = Type.Record(Type.Number(), Type.Null(), { + additionalProperties: false, + }) + const R1 = Value.Check(T, { + a: null, + b: null, + 0: null, + 1: null, + }) + const R2 = Value.Check(T, { + 0: null, + 1: null, + }) + Assert.IsEqual(R1, false) + Assert.IsEqual(R2, true) + }) + it('Should validate for any keys', () => { + const T = Type.Record(Type.Any(), Type.Null(), { + additionalProperties: false, + }) + const R = Value.Check(T, { + a: null, + b: null, + 0: null, + 1: null, + }) + Assert.IsEqual(R, true) + }) + it('Should validate for never keys', () => { + const T = Type.Record(Type.Never(), Type.Null(), { + additionalProperties: false, + }) + const R1 = Value.Check(T, {}) + const R2 = Value.Check(T, { + a: null, + b: null, + 0: null, + 1: null, + }) + Assert.IsEqual(R1, true) + Assert.IsEqual(R2, false) + }) +}) diff --git a/test/runtime/value/check/recursive.ts b/test/runtime/value/check/recursive.ts new file mode 100644 index 000000000..1b6c60bb9 --- /dev/null +++ b/test/runtime/value/check/recursive.ts @@ -0,0 +1,69 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/check/Recursive', () => { + const T = Type.Recursive((This) => + Type.Object({ + id: Type.String(), + nodes: Type.Array(This), + }), + ) + + it('Should pass recursive', () => { + const value = { + id: 'A', + nodes: [ + { id: 'B', nodes: [] }, + { id: 'C', nodes: [] }, + { id: 'D', nodes: [] }, + ], + } + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + + it('Should fail recursive with invalid id', () => { + const value = { + id: 'A', + nodes: [ + { id: 1, nodes: [] }, + { id: 'C', nodes: [] }, + { id: 'D', nodes: [] }, + ], + } + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + + it('Should fail rec with invalid nodes', () => { + const value = { + id: 'A', + nodes: [ + { id: 'B', nodes: 1 }, + { id: 'C', nodes: [] }, + { id: 'D', nodes: [] }, + ], + } + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + + it('Should fail recursive with missing id', () => { + const value = { + id: 'A', + nodes: [{ nodes: [] }, { id: 'C', nodes: [] }, { id: 'D', nodes: [] }], + } + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + + it('Should fail rec with missing nodes', () => { + const value = { + id: 'A', + nodes: [{ id: 'B' }, { id: 'C', nodes: [] }, { id: 'D', nodes: [] }], + } + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) +}) diff --git a/test/runtime/value/check/ref.ts b/test/runtime/value/check/ref.ts new file mode 100644 index 000000000..aa7d76f03 --- /dev/null +++ b/test/runtime/value/check/ref.ts @@ -0,0 +1,95 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/check/Ref', () => { + // ---------------------------------------------------------------- + // Deprecated + // ---------------------------------------------------------------- + it('Should validate for Ref(Schema)', () => { + const T = Type.Number({ $id: 'T' }) + const R = Type.Ref(T) + Assert.IsTrue(Value.Check(T, [T], 1234)) + Assert.IsFalse(Value.Check(T, [T], 'hello')) + }) + // ---------------------------------------------------------------- + // Standard + // ---------------------------------------------------------------- + it('Should should validate when referencing a type', () => { + const T = Type.Object( + { + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }, + { $id: Assert.NextId() }, + ) + const R = Type.Ref(T.$id!) + Assert.IsEqual( + Value.Check(R, [T], { + x: 1, + y: 2, + z: 3, + }), + true, + ) + }) + + it('Should not validate when passing invalid data', () => { + const T = Type.Object( + { + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }, + { $id: Assert.NextId() }, + ) + const R = Type.Ref(T.$id!) + Assert.IsEqual( + Value.Check(R, [T], { + x: 1, + y: 2, + }), + false, + ) + }) + + it('Should de-reference object property schema', () => { + const T = Type.Object( + { + name: Type.String(), + }, + { $id: 'R' }, + ) + + const R = Type.Object( + { + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + r: Type.Optional(Type.Ref(T.$id!)), + }, + { $id: 'T' }, + ) + Assert.IsEqual(Value.Check(R, [T], { x: 1, y: 2, z: 3 }), true) + Assert.IsEqual(Value.Check(R, [T], { x: 1, y: 2, z: 3, r: { name: 'hello' } }), true) + Assert.IsEqual(Value.Check(R, [T], { x: 1, y: 2, z: 3, r: { name: 1 } }), false) + Assert.IsEqual(Value.Check(R, [T], { x: 1, y: 2, z: 3, r: {} }), false) + // Ok(R, { x: 1, y: 2, z: 3 }, [T]) + // Ok(R, , [T]) + // Fail(R, , [T]) + // Fail(R, , [T]) + }) + + it('Should reference recursive schema', () => { + const T = Type.Recursive((Node) => + Type.Object({ + id: Type.String(), + nodes: Type.Array(Node), + }), + ) + const R = Type.Ref(T.$id!) + Assert.IsEqual(Value.Check(R, [T], { id: '', nodes: [{ id: '', nodes: [] }] }), true) + Assert.IsEqual(Value.Check(R, [T], { id: '', nodes: [{ id: 1, nodes: [] }] }), false) + }) +}) diff --git a/test/runtime/value/check/regexp.ts b/test/runtime/value/check/regexp.ts new file mode 100644 index 000000000..f25958126 --- /dev/null +++ b/test/runtime/value/check/regexp.ts @@ -0,0 +1,50 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/check/RegExp', () => { + // ------------------------------------------------- + // Regular Expression + // ------------------------------------------------- + it('Should pass regular expression 1', () => { + const T = Type.RegExp(/foo/) + const value = 'foo' + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + it('Should pass regular expression 2', () => { + const T = Type.RegExp(/foo/) + const value = 'bar' + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + // ------------------------------------------------- + // Pattern + // ------------------------------------------------- + it('Should pass pattern 1', () => { + const T = Type.RegExp('foo') + const value = 'foo' + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + it('Should pass pattern 2', () => { + const T = Type.RegExp('foo') + const value = 'bar' + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should validate with minLength constraint', () => { + const T = Type.RegExp(/(.*)/, { + minLength: 3, + }) + Assert.IsTrue(Value.Check(T, 'xxx')) + Assert.IsFalse(Value.Check(T, 'xx')) + }) + it('Should validate with maxLength constraint', () => { + const T = Type.RegExp(/(.*)/, { + maxLength: 3, + }) + Assert.IsTrue(Value.Check(T, 'xxx')) + Assert.IsFalse(Value.Check(T, 'xxxx')) + }) +}) diff --git a/test/runtime/value/check/string.ts b/test/runtime/value/check/string.ts new file mode 100644 index 000000000..48a0eb6a9 --- /dev/null +++ b/test/runtime/value/check/string.ts @@ -0,0 +1,47 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/check/String', () => { + const T = Type.String() + it('Should pass string', () => { + const value = 'hello' + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + it('Should fail number', () => { + const value = 1 + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail boolean', () => { + const value = true + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail null', () => { + const value = null + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail undefined', () => { + const value = undefined + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail object', () => { + const value = { a: 1 } + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail array', () => { + const value = [1, 2] + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail Date', () => { + const value = new Date() + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) +}) diff --git a/test/runtime/value/check/symbol.ts b/test/runtime/value/check/symbol.ts new file mode 100644 index 000000000..ad342813d --- /dev/null +++ b/test/runtime/value/check/symbol.ts @@ -0,0 +1,52 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/check/Symbol', () => { + const T = Type.Symbol() + it('Should fail string', () => { + const value = 'hello' + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail number', () => { + const value = 1 + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail boolean', () => { + const value = true + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail null', () => { + const value = null + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail undefined', () => { + const value = undefined + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail object', () => { + const value = { a: 1 } + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail array', () => { + const value = [1, 2] + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail Date', () => { + const value = new Date() + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should pass symbol', () => { + const value = Symbol(1) + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) +}) diff --git a/test/runtime/value/check/template-literal.ts b/test/runtime/value/check/template-literal.ts new file mode 100644 index 000000000..0cffa6f86 --- /dev/null +++ b/test/runtime/value/check/template-literal.ts @@ -0,0 +1,187 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/check/TemplateLiteral', () => { + // -------------------------------------------------------- + // Finite + // -------------------------------------------------------- + it('Should validate finite pattern 1', () => { + // prettier-ignore + const T = Type.TemplateLiteral([]) + Assert.IsEqual(Value.Check(T, ''), true) + Assert.IsEqual(Value.Check(T, 'X'), false) + }) + it('Should validate finite pattern 1', () => { + // prettier-ignore + const T = Type.TemplateLiteral([Type.Boolean()]) + Assert.IsEqual(Value.Check(T, 'true'), true) + Assert.IsEqual(Value.Check(T, 'false'), true) + Assert.IsEqual(Value.Check(T, 'X'), false) + }) + it('Should validate finite pattern 2', () => { + // prettier-ignore + const T = Type.TemplateLiteral([ + Type.Literal('A') + ]) + Assert.IsEqual(Value.Check(T, 'A'), true) + Assert.IsEqual(Value.Check(T, 'X'), false) + }) + it('Should validate finite pattern 3', () => { + // prettier-ignore + const T = Type.TemplateLiteral([ + Type.Literal('A'), + Type.Literal('B') + ]) + Assert.IsEqual(Value.Check(T, 'AB'), true) + Assert.IsEqual(Value.Check(T, 'X'), false) + }) + it('Should validate finite pattern 4', () => { + // prettier-ignore + const T = Type.TemplateLiteral([ + Type.Literal('A'), + Type.Union([ + Type.Literal('B'), + Type.Literal('C') + ]), + ]) + Assert.IsEqual(Value.Check(T, 'AB'), true) + Assert.IsEqual(Value.Check(T, 'AC'), true) + Assert.IsEqual(Value.Check(T, 'X'), false) + }) + it('Should validate finite pattern 5', () => { + // prettier-ignore + const T = Type.TemplateLiteral([ + Type.Literal('A'), + Type.Union([ + Type.Literal('B'), + Type.Literal('C') + ]), + Type.Literal('D'), + ]) + Assert.IsEqual(Value.Check(T, 'ABD'), true) + Assert.IsEqual(Value.Check(T, 'ACD'), true) + Assert.IsEqual(Value.Check(T, 'X'), false) + }) + it('Should validate finite pattern 6', () => { + // prettier-ignore + const T = Type.TemplateLiteral([ + Type.Union([ + Type.Literal('0'), + Type.Literal('1') + ]), + Type.Union([ + Type.Literal('0'), + Type.Literal('1') + ]), + ]) + Assert.IsEqual(Value.Check(T, '00'), true) + Assert.IsEqual(Value.Check(T, '01'), true) + Assert.IsEqual(Value.Check(T, '10'), true) + Assert.IsEqual(Value.Check(T, '11'), true) + Assert.IsEqual(Value.Check(T, 'X'), false) + }) + // -------------------------------------------------------- + // Infinite + // -------------------------------------------------------- + it('Should validate infinite pattern 1', () => { + // prettier-ignore + const T = Type.TemplateLiteral([ + Type.Number() + ]) + Assert.IsEqual(Value.Check(T, '1'), true) + Assert.IsEqual(Value.Check(T, '22'), true) + Assert.IsEqual(Value.Check(T, '333'), true) + Assert.IsEqual(Value.Check(T, '4444'), true) + Assert.IsEqual(Value.Check(T, 'X'), false) + }) + it('Should validate infinite pattern 2', () => { + // prettier-ignore + const T = Type.TemplateLiteral([ + Type.Integer() + ]) + Assert.IsEqual(Value.Check(T, '1'), true) + Assert.IsEqual(Value.Check(T, '22'), true) + Assert.IsEqual(Value.Check(T, '333'), true) + Assert.IsEqual(Value.Check(T, '4444'), true) + Assert.IsEqual(Value.Check(T, 'X'), false) + }) + it('Should validate infinite pattern 3', () => { + // prettier-ignore + const T = Type.TemplateLiteral([ + Type.BigInt() + ]) + Assert.IsEqual(Value.Check(T, '1'), true) + Assert.IsEqual(Value.Check(T, '22'), true) + Assert.IsEqual(Value.Check(T, '333'), true) + Assert.IsEqual(Value.Check(T, '4444'), true) + Assert.IsEqual(Value.Check(T, 'X'), false) + }) + it('Should validate infinite pattern 4', () => { + // prettier-ignore + const T = Type.TemplateLiteral([ + Type.String() + ]) + Assert.IsEqual(Value.Check(T, '1'), true) + Assert.IsEqual(Value.Check(T, '22'), true) + Assert.IsEqual(Value.Check(T, '333'), true) + Assert.IsEqual(Value.Check(T, '4444'), true) + Assert.IsEqual(Value.Check(T, 'a'), true) + Assert.IsEqual(Value.Check(T, 'bb'), true) + Assert.IsEqual(Value.Check(T, 'ccc'), true) + Assert.IsEqual(Value.Check(T, 'dddd'), true) + }) + + it('Should validate infinite pattern 5', () => { + // prettier-ignore + const T = Type.TemplateLiteral([ + Type.Literal('A'), + Type.Number() + ]) + Assert.IsEqual(Value.Check(T, 'A1'), true) + Assert.IsEqual(Value.Check(T, 'A22'), true) + Assert.IsEqual(Value.Check(T, 'A333'), true) + Assert.IsEqual(Value.Check(T, 'A4444'), true) + Assert.IsEqual(Value.Check(T, 'X'), false) + }) + it('Should validate infinite pattern 6', () => { + // prettier-ignore + const T = Type.TemplateLiteral([ + Type.Literal('A'), + Type.Integer() + ]) + Assert.IsEqual(Value.Check(T, 'A1'), true) + Assert.IsEqual(Value.Check(T, 'A22'), true) + Assert.IsEqual(Value.Check(T, 'A333'), true) + Assert.IsEqual(Value.Check(T, 'A4444'), true) + Assert.IsEqual(Value.Check(T, 'X'), false) + }) + it('Should validate infinite pattern 7', () => { + // prettier-ignore + const T = Type.TemplateLiteral([ + Type.Literal('A'), + Type.BigInt() + ]) + Assert.IsEqual(Value.Check(T, 'A1'), true) + Assert.IsEqual(Value.Check(T, 'A22'), true) + Assert.IsEqual(Value.Check(T, 'A333'), true) + Assert.IsEqual(Value.Check(T, 'A4444'), true) + Assert.IsEqual(Value.Check(T, 'X'), false) + }) + it('Should validate infinite pattern 8', () => { + // prettier-ignore + const T = Type.TemplateLiteral([ + Type.Literal('A'), + Type.String() + ]) + Assert.IsEqual(Value.Check(T, 'A1'), true) + Assert.IsEqual(Value.Check(T, 'A22'), true) + Assert.IsEqual(Value.Check(T, 'A333'), true) + Assert.IsEqual(Value.Check(T, 'A4444'), true) + Assert.IsEqual(Value.Check(T, 'Aa'), true) + Assert.IsEqual(Value.Check(T, 'Abb'), true) + Assert.IsEqual(Value.Check(T, 'Accc'), true) + Assert.IsEqual(Value.Check(T, 'Adddd'), true) + Assert.IsEqual(Value.Check(T, 'X'), false) + }) +}) diff --git a/test/runtime/value/check/tuple.ts b/test/runtime/value/check/tuple.ts new file mode 100644 index 000000000..1a9f039c8 --- /dev/null +++ b/test/runtime/value/check/tuple.ts @@ -0,0 +1,40 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/check/Tuple', () => { + it('Should pass tuple', () => { + const T = Type.Tuple([Type.Number(), Type.Number()]) + const value = [1, 2] + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + + it('Should fail when tuple is less than length', () => { + const T = Type.Tuple([Type.Number(), Type.Number()]) + const value = [1] + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + + it('Should fail when tuple is greater than length', () => { + const T = Type.Tuple([Type.Number(), Type.Number()]) + const value = [1, 1, 2] + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + + it('Should pass empty tuple', () => { + const T = Type.Tuple([]) + const value = [] as any[] + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + + it('Should fail empty tuple', () => { + const T = Type.Tuple([]) + const value = [1] + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) +}) diff --git a/test/runtime/value/check/uint8array.ts b/test/runtime/value/check/uint8array.ts new file mode 100644 index 000000000..c989fa966 --- /dev/null +++ b/test/runtime/value/check/uint8array.ts @@ -0,0 +1,33 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/check/Uint8Array', () => { + it('Should pass Uint8Array', () => { + const T = Type.Uint8Array() + const value = new Uint8Array(4) + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + + it('Should fail Uint8Array', () => { + const T = Type.Uint8Array() + const value = [0, 1, 2, 3] + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + + it('Should fail Uint8Array with undefined', () => { + const T = Type.Uint8Array() + const value = undefined + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + + it('Should fail Uint8Array with null', () => { + const T = Type.Uint8Array() + const value = null + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) +}) diff --git a/test/runtime/value/check/undefined.ts b/test/runtime/value/check/undefined.ts new file mode 100644 index 000000000..bdc0099c0 --- /dev/null +++ b/test/runtime/value/check/undefined.ts @@ -0,0 +1,47 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/check/Undefined', () => { + const T = Type.Undefined() + it('Should fail string', () => { + const value = 'hello' + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail number', () => { + const value = 1 + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail boolean', () => { + const value = true + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail null', () => { + const value = null + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should pass undefined', () => { + const value = undefined + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + it('Should fail object', () => { + const value = { a: 1 } + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail array', () => { + const value = [1, 2] + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail Date', () => { + const value = new Date() + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) +}) diff --git a/test/runtime/value/check/union.ts b/test/runtime/value/check/union.ts new file mode 100644 index 000000000..c5a31b653 --- /dev/null +++ b/test/runtime/value/check/union.ts @@ -0,0 +1,77 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/check/Union', () => { + const A = Type.Object({ + type: Type.Literal('A'), + x: Type.Number(), + y: Type.Number(), + }) + + const B = Type.Object({ + type: Type.Literal('B'), + x: Type.Boolean(), + y: Type.Boolean(), + }) + + const T = Type.Union([A, B]) + + it('Should pass union A', () => { + const value = { type: 'A', x: 1, y: 1 } + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + + it('Should pass union B', () => { + const value = { type: 'B', x: true, y: false } + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + + it('Should fail union A', () => { + const value = { type: 'A', x: true, y: false } + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + + it('Should fail union B', () => { + const value = { type: 'B', x: 1, y: 1 } + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + + it('Should pass union A with optional properties', () => { + const A = Type.Object({ + type: Type.Literal('A'), + x: Type.Optional(Type.Number()), + y: Type.Optional(Type.Number()), + }) + const B = Type.Object({ + type: Type.Literal('B'), + x: Type.Boolean(), + y: Type.Boolean(), + }) + const T = Type.Union([A, B]) + const value = { type: 'A' } + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + + it('Should fail union A with invalid optional properties', () => { + const A = Type.Object({ + type: Type.Literal('A'), + x: Type.Optional(Type.Number()), + y: Type.Optional(Type.Number()), + }) + const B = Type.Object({ + type: Type.Literal('B'), + x: Type.Boolean(), + y: Type.Boolean(), + }) + const T = Type.Union([A, B]) + const value = { type: 'A', x: true, y: false } + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) +}) diff --git a/test/runtime/value/check/unknown.ts b/test/runtime/value/check/unknown.ts new file mode 100644 index 000000000..7026f3dcc --- /dev/null +++ b/test/runtime/value/check/unknown.ts @@ -0,0 +1,47 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/check/Unknown', () => { + const T = Type.Any() + it('Should pass string', () => { + const value = 'hello' + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + it('Should pass number', () => { + const value = 1 + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + it('Should pass boolean', () => { + const value = true + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + it('Should pass null', () => { + const value = null + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + it('Should pass undefined', () => { + const value = undefined + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + it('Should pass object', () => { + const value = { a: 1 } + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + it('Should pass array', () => { + const value = [1, 2] + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + it('Should pass Date', () => { + const value = new Date() + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) +}) diff --git a/test/runtime/value/check/void.ts b/test/runtime/value/check/void.ts new file mode 100644 index 000000000..92ff30b74 --- /dev/null +++ b/test/runtime/value/check/void.ts @@ -0,0 +1,47 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/check/Void', () => { + const T = Type.Void() + it('Should fail string', () => { + const value = 'hello' + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail number', () => { + const value = 1 + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail boolean', () => { + const value = true + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should pass null', () => { + const value = null + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should pass undefined', () => { + const value = undefined + const result = Value.Check(T, value) + Assert.IsEqual(result, true) + }) + it('Should fail object', () => { + const value = { a: 1 } + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail array', () => { + const value = [1, 2] + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) + it('Should fail Date', () => { + const value = new Date() + const result = Value.Check(T, value) + Assert.IsEqual(result, false) + }) +}) diff --git a/test/runtime/value/clean/any.ts b/test/runtime/value/clean/any.ts new file mode 100644 index 000000000..9a49a2562 --- /dev/null +++ b/test/runtime/value/clean/any.ts @@ -0,0 +1,11 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/clean/Any', () => { + it('Should clean 1', () => { + const T = Type.Any() + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/clean/array.ts b/test/runtime/value/clean/array.ts new file mode 100644 index 000000000..6e8a21844 --- /dev/null +++ b/test/runtime/value/clean/array.ts @@ -0,0 +1,21 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/clean/Array', () => { + it('Should clean 1', () => { + const T = Type.Any() + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) + it('Should clean 2', () => { + const T = Type.Array( + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + ) + const R = Value.Clean(T, [undefined, null, { x: 1 }, { x: 1, y: 2 }, { x: 1, y: 2, z: 3 }]) + Assert.IsEqual(R, [undefined, null, { x: 1 }, { x: 1, y: 2 }, { x: 1, y: 2 }]) + }) +}) diff --git a/test/runtime/value/clean/async-iterator.ts b/test/runtime/value/clean/async-iterator.ts new file mode 100644 index 000000000..a067233b3 --- /dev/null +++ b/test/runtime/value/clean/async-iterator.ts @@ -0,0 +1,11 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/clean/AsyncIterator', () => { + it('Should clean 1', () => { + const T = Type.AsyncIterator(Type.Number()) + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/clean/bigint.ts b/test/runtime/value/clean/bigint.ts new file mode 100644 index 000000000..cdd77d4de --- /dev/null +++ b/test/runtime/value/clean/bigint.ts @@ -0,0 +1,11 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/clean/BigInt', () => { + it('Should clean 1', () => { + const T = Type.BigInt() + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/clean/boolean.ts b/test/runtime/value/clean/boolean.ts new file mode 100644 index 000000000..7697d25c3 --- /dev/null +++ b/test/runtime/value/clean/boolean.ts @@ -0,0 +1,11 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/clean/Boolean', () => { + it('Should clean 1', () => { + const T = Type.Boolean() + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/clean/composite.ts b/test/runtime/value/clean/composite.ts new file mode 100644 index 000000000..035f335a9 --- /dev/null +++ b/test/runtime/value/clean/composite.ts @@ -0,0 +1,11 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/clean/Composite', () => { + it('Should clean 1', () => { + const T = Type.Composite([Type.Object({ x: Type.Number() }), Type.Object({ y: Type.Number() })]) + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/clean/constructor.ts b/test/runtime/value/clean/constructor.ts new file mode 100644 index 000000000..4a4be492c --- /dev/null +++ b/test/runtime/value/clean/constructor.ts @@ -0,0 +1,11 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/clean/Constructor', () => { + it('Should clean 1', () => { + const T = Type.Constructor([Type.Object({ x: Type.Number() }), Type.Object({ y: Type.Number() })], Type.Object({ z: Type.Number() })) + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/clean/date.ts b/test/runtime/value/clean/date.ts new file mode 100644 index 000000000..8f777e4c3 --- /dev/null +++ b/test/runtime/value/clean/date.ts @@ -0,0 +1,11 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/clean/Date', () => { + it('Should clean 1', () => { + const T = Type.Date() + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/clean/enum.ts b/test/runtime/value/clean/enum.ts new file mode 100644 index 000000000..88902047b --- /dev/null +++ b/test/runtime/value/clean/enum.ts @@ -0,0 +1,11 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/clean/Enum', () => { + it('Should clean 1', () => { + const T = Type.Enum({ x: 1, y: 2 }) + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/clean/function.ts b/test/runtime/value/clean/function.ts new file mode 100644 index 000000000..2bc5a4ee5 --- /dev/null +++ b/test/runtime/value/clean/function.ts @@ -0,0 +1,11 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/clean/Function', () => { + it('Should clean 1', () => { + const T = Type.Function([Type.Object({ x: Type.Number() }), Type.Object({ y: Type.Number() })], Type.Object({ z: Type.Number() })) + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/clean/import.ts b/test/runtime/value/clean/import.ts new file mode 100644 index 000000000..1ccc7c81d --- /dev/null +++ b/test/runtime/value/clean/import.ts @@ -0,0 +1,203 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/clean/Import', () => { + // ---------------------------------------------------------------- + // Clean + // ---------------------------------------------------------------- + it('Should clean 1', () => { + const T = Type.Module({ A: Type.Object({ x: Type.Number() }) }).Import('A') + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) + it('Should clean 2', () => { + const T = Type.Module({ A: Type.Object({ x: Type.Number() }) }).Import('A') + const R = Value.Clean(T, {}) + Assert.IsEqual(R, {}) + }) + it('Should clean 3', () => { + const T = Type.Module({ A: Type.Object({ x: Type.Number() }) }).Import('A') + const R = Value.Clean(T, { x: 1 }) + Assert.IsEqual(R, { x: 1 }) + }) + it('Should clean 4', () => { + const T = Type.Module({ A: Type.Object({ x: Type.Number() }) }).Import('A') + const R = Value.Clean(T, { x: null }) + Assert.IsEqual(R, { x: null }) + }) + // ---------------------------------------------------------------- + // Nested + // ---------------------------------------------------------------- + it('Should clean nested 1', () => { + const T = Type.Module({ + A: Type.Object({ + x: Type.Object({ + y: Type.Number(), + }), + }), + }).Import('A') + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) + it('Should clean nested 2', () => { + const T = Type.Module({ + A: Type.Object({ + x: Type.Object({ + y: Type.Number(), + }), + }), + }).Import('A') + const R = Value.Clean(T, {}) + Assert.IsEqual(R, {}) + }) + it('Should clean nested 3', () => { + const T = Type.Module({ + A: Type.Object({ + x: Type.Object({ + y: Type.Number(), + }), + }), + }).Import('A') + + const R = Value.Clean(T, { x: null }) + Assert.IsEqual(R, { x: null }) + }) + it('Should clean nested 4', () => { + const T = Type.Module({ + A: Type.Object({ + x: Type.Object({ + y: Type.Number(), + }), + }), + }).Import('A') + const R = Value.Clean(T, { x: { y: null } }) + Assert.IsEqual(R, { x: { y: null } }) + }) + // ---------------------------------------------------------------- + // Additional Properties + // ---------------------------------------------------------------- + it('Should clean additional properties 1', () => { + const T = Type.Module({ + A: Type.Object( + { + x: Type.Number(), + y: Type.Number(), + }, + { + additionalProperties: Type.String(), + }, + ), + }).Import('A') + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) + it('Should clean additional properties 2', () => { + const T = Type.Module({ + A: Type.Object( + { + x: Type.Number(), + y: Type.Number(), + }, + { + additionalProperties: Type.String(), + }, + ), + }).Import('A') + const R = Value.Clean(T, {}) + Assert.IsEqual(R, {}) + }) + it('Should clean additional properties 3', () => { + const T = Type.Module({ + A: Type.Object( + { + x: Type.Number(), + y: Type.Number(), + }, + { + additionalProperties: Type.String(), + }, + ), + }).Import('A') + const R = Value.Clean(T, { x: 1 }) + Assert.IsEqual(R, { x: 1 }) + }) + it('Should clean additional properties 4', () => { + const T = Type.Module({ + A: Type.Object( + { + x: Type.Number(), + y: Type.Number(), + }, + { + additionalProperties: Type.String(), + }, + ), + }).Import('A') + const R = Value.Clean(T, { x: 1, y: 2 }) + Assert.IsEqual(R, { x: 1, y: 2 }) + }) + // ---------------------------------------------------------------- + // Additional Properties Discard + // ---------------------------------------------------------------- + it('Should clean additional properties discard 1', () => { + const T = Type.Module({ + A: Type.Object( + { + x: Type.Number(), + y: Type.Number(), + }, + { + additionalProperties: Type.String(), + }, + ), + }).Import('A') + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) + it('Should clean additional properties discard 2', () => { + const T = Type.Module({ + A: Type.Object( + { + x: Type.Number(), + y: Type.Number(), + }, + { + additionalProperties: Type.String(), + }, + ), + }).Import('A') + const R = Value.Clean(T, { k: '', d: null }) + Assert.IsEqual(R, { k: '' }) + }) + it('Should clean additional properties discard 3', () => { + const T = Type.Module({ + A: Type.Object( + { + x: Type.Number(), + y: Type.Number(), + }, + { + additionalProperties: Type.String(), + }, + ), + }).Import('A') + const R = Value.Clean(T, { k: '', d: null, x: 1 }) + Assert.IsEqual(R, { k: '', x: 1 }) + }) + it('Should clean additional properties discard 4', () => { + const T = Type.Module({ + A: Type.Object( + { + x: Type.Number(), + y: Type.Number(), + }, + { + additionalProperties: Type.String(), + }, + ), + }).Import('A') + const R = Value.Clean(T, { k: '', d: null, x: 1, y: 2 }) + Assert.IsEqual(R, { k: '', x: 1, y: 2 }) + }) +}) diff --git a/test/runtime/value/clean/index.ts b/test/runtime/value/clean/index.ts new file mode 100644 index 000000000..99af15b9c --- /dev/null +++ b/test/runtime/value/clean/index.ts @@ -0,0 +1,35 @@ +import './any' +import './array' +import './async-iterator' +import './bigint' +import './boolean' +import './composite' +import './constructor' +import './date' +import './enum' +import './function' +import './import' +import './integer' +import './intersect' +import './iterator' +import './keyof' +import './kind' +import './literal' +import './never' +import './not' +import './null' +import './object' +import './promise' +import './record' +import './recursive' +import './ref' +import './regexp' +import './string' +import './symbol' +import './template-literal' +import './tuple' +import './uint8array' +import './undefined' +import './union' +import './unknown' +import './void' diff --git a/test/runtime/value/clean/integer.ts b/test/runtime/value/clean/integer.ts new file mode 100644 index 000000000..ff377049c --- /dev/null +++ b/test/runtime/value/clean/integer.ts @@ -0,0 +1,11 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/clean/Integer', () => { + it('Should clean 1', () => { + const T = Type.Integer() + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/clean/intersect.ts b/test/runtime/value/clean/intersect.ts new file mode 100644 index 000000000..14f5d5221 --- /dev/null +++ b/test/runtime/value/clean/intersect.ts @@ -0,0 +1,346 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +// prettier-ignore +describe('value/clean/Intersect', () => { + // ---------------------------------------------------------------- + // Intersect + // ---------------------------------------------------------------- + it('Should clean 1', () => { + const T = Type.Intersect([ + Type.Object({ x: Type.Number() }), + Type.Object({ y: Type.Number() }) + ]) + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) + it('Should clean 2', () => { + const T = Type.Intersect([ + Type.Object({ x: Type.Number() }), + Type.Object({ y: Type.Number() }) + ]) + const R = Value.Clean(T, {}) + Assert.IsEqual(R, {}) + }) + it('Should clean 3', () => { + const T = Type.Intersect([ + Type.Object({ x: Type.Number() }), + Type.Object({ y: Type.Number() }) + ]) + const R = Value.Clean(T, { x: 1, y: 2 }) + Assert.IsEqual(R, { x: 1, y: 2 }) + }) + it('Should clean 4', () => { + const T = Type.Intersect([ + Type.Object({ x: Type.Number() }), + Type.Object({ y: Type.Number() }) + ]) + const R = Value.Clean(T, { x: 1, y: 2, z: 3 }) + Assert.IsEqual(R, { x: 1, y: 2 }) + }) + // ---------------------------------------------------------------- + // Intersect Discard + // ---------------------------------------------------------------- + it('Should clean discard 1', () => { + const T = Type.Intersect([ + Type.Object({ x: Type.Number() }), + Type.Object({ y: Type.Number() }) + ]) + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) + it('Should clean discard 2', () => { + const T = Type.Intersect([ + Type.Object({ x: Type.Number() }), + Type.Object({ y: Type.Number() }) + ]) + const R = Value.Clean(T, { u: null }) + Assert.IsEqual(R, {}) + }) + it('Should clean discard 3', () => { + const T = Type.Intersect([ + Type.Object({ x: Type.Number() }), + Type.Object({ y: Type.Number() }) + ]) + const R = Value.Clean(T, { u: null, x: 1, y: 2 }) + Assert.IsEqual(R, { x: 1, y: 2 }) + }) + it('Should clean discard 4', () => { + const T = Type.Intersect([ + Type.Object({ x: Type.Number() }), + Type.Object({ y: Type.Number() }) + ]) + const R = Value.Clean(T, { u: null, x: 1, y: 2 }) + Assert.IsEqual(R, { x: 1, y: 2 }) + }) + // ---------------------------------------------------------------- + // Intersect Deep + // ---------------------------------------------------------------- + it('Should clear intersect deep 1', () => { + const T = Type.Intersect([ + Type.Intersect([ + Type.Object({ x: Type.Number() }), + Type.Object({ y: Type.Number() }) + ]), + Type.Intersect([ + Type.Object({ z: Type.Number() }), + Type.Object({ w: Type.Number() }) + ]) + ]) + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) + it('Should clear intersect deep 2', () => { + const T = Type.Intersect([ + Type.Intersect([ + Type.Object({ x: Type.Number() }), + Type.Object({ y: Type.Number() }) + ]), + Type.Intersect([ + Type.Object({ z: Type.Number() }), + Type.Object({ w: Type.Number() }) + ]) + ]) + const R = Value.Clean(T, {}) + Assert.IsEqual(R, {}) + }) + it('Should clear intersect deep 3', () => { + const T = Type.Intersect([ + Type.Intersect([ + Type.Object({ x: Type.Number() }), + Type.Object({ y: Type.Number() }) + ]), + Type.Intersect([ + Type.Object({ z: Type.Number() }), + Type.Object({ w: Type.Number() }) + ]) + ]) + const R = Value.Clean(T, { x: 1 }) + Assert.IsEqual(R, { x: 1 }) + }) + it('Should clear intersect deep 4', () => { + const T = Type.Intersect([ + Type.Intersect([ + Type.Object({ x: Type.Number() }), + Type.Object({ y: Type.Number() }) + ]), + Type.Intersect([ + Type.Object({ z: Type.Number() }), + Type.Object({ w: Type.Number() }) + ]) + ]) + const R = Value.Clean(T, { x: 1, y: 2 }) + Assert.IsEqual(R, { x: 1, y: 2 }) + }) + it('Should clear intersect deep 5', () => { + const T = Type.Intersect([ + Type.Intersect([ + Type.Object({ x: Type.Number() }), + Type.Object({ y: Type.Number() }) + ]), + Type.Intersect([ + Type.Object({ z: Type.Number() }), + Type.Object({ w: Type.Number() }) + ]) + ]) + const R = Value.Clean(T, { x: 1, y: 2, z: 3 }) + Assert.IsEqual(R, { x: 1, y: 2, z: 3 }) + }) + it('Should clear intersect deep 6', () => { + const T = Type.Intersect([ + Type.Intersect([ + Type.Object({ x: Type.Number() }), + Type.Object({ y: Type.Number() }) + ]), + Type.Intersect([ + Type.Object({ z: Type.Number() }), + Type.Object({ w: Type.Number() }) + ]) + ]) + const R = Value.Clean(T, { x: 1, y: 2, z: 3, w: 3 }) + Assert.IsEqual(R, { x: 1, y: 2, z: 3, w: 3 }) + }) + it('Should clear intersect deep 7', () => { + const T = Type.Intersect([ + Type.Intersect([ + Type.Object({ x: Type.Number() }), + Type.Object({ y: Type.Number() }) + ]), + Type.Intersect([ + Type.Object({ z: Type.Number() }), + Type.Object({ w: Type.Number() }) + ]) + ]) + const R = Value.Clean(T, { x: 1, y: 2, z: 3, w: 3 }) + Assert.IsEqual(R, { x: 1, y: 2, z: 3, w: 3 }) + }) + // ---------------------------------------------------------------- + // Intersect Deep Discard + // ---------------------------------------------------------------- + it('Should clear intersect discard deep 1', () => { + const T = Type.Intersect([ + Type.Intersect([ + Type.Object({ x: Type.Number() }), + Type.Object({ y: Type.Number() }) + ]), + Type.Intersect([ + Type.Object({ z: Type.Number() }), + Type.Object({ w: Type.Number() }) + ]) + ]) + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) + it('Should clear intersect discard deep 2', () => { + const T = Type.Intersect([ + Type.Intersect([ + Type.Object({ x: Type.Number() }), + Type.Object({ y: Type.Number() }) + ]), + Type.Intersect([ + Type.Object({ z: Type.Number() }), + Type.Object({ w: Type.Number() }) + ]) + ]) + const R = Value.Clean(T, { u: null }) + Assert.IsEqual(R, {}) + }) + it('Should clear intersect discard deep 3', () => { + const T = Type.Intersect([ + Type.Intersect([ + Type.Object({ x: Type.Number() }), + Type.Object({ y: Type.Number() }) + ]), + Type.Intersect([ + Type.Object({ z: Type.Number() }), + Type.Object({ w: Type.Number() }) + ]) + ]) + const R = Value.Clean(T, { u: null, x: 1 }) + Assert.IsEqual(R, { x: 1 }) + }) + it('Should clear intersect discard deep 4', () => { + const T = Type.Intersect([ + Type.Intersect([ + Type.Object({ x: Type.Number() }), + Type.Object({ y: Type.Number() }) + ]), + Type.Intersect([ + Type.Object({ z: Type.Number() }), + Type.Object({ w: Type.Number() }) + ]) + ]) + const R = Value.Clean(T, { u: null, x: 1, y: 2 }) + Assert.IsEqual(R, { x: 1, y: 2 }) + }) + it('Should clear intersect discard deep 5', () => { + const T = Type.Intersect([ + Type.Intersect([ + Type.Object({ x: Type.Number() }), + Type.Object({ y: Type.Number() }) + ]), + Type.Intersect([ + Type.Object({ z: Type.Number() }), + Type.Object({ w: Type.Number() }) + ]) + ]) + const R = Value.Clean(T, { u: null, x: 1, y: 2, z: 3 }) + Assert.IsEqual(R, { x: 1, y: 2, z: 3 }) + }) + it('Should clear intersect discard deep 6', () => { + const T = Type.Intersect([ + Type.Intersect([ + Type.Object({ x: Type.Number() }), + Type.Object({ y: Type.Number() }) + ]), + Type.Intersect([ + Type.Object({ z: Type.Number() }), + Type.Object({ w: Type.Number() }) + ]) + ]) + const R = Value.Clean(T, { u: null, x: 1, y: 2, z: 3, w: 3 }) + Assert.IsEqual(R, { x: 1, y: 2, z: 3, w: 3 }) + }) + it('Should clear intersect discard deep 7', () => { + const T = Type.Intersect([ + Type.Intersect([ + Type.Object({ x: Type.Number() }), + Type.Object({ y: Type.Number() }) + ]), + Type.Intersect([ + Type.Object({ z: Type.Number() }), + Type.Object({ w: Type.Number() }) + ]) + ]) + const R = Value.Clean(T, { u: null, x: 1, y: 2, z: 3, w: 3, a: 1 }) + Assert.IsEqual(R, { x: 1, y: 2, z: 3, w: 3 }) + }) + // ---------------------------------------------------------------- + // Intersect Invalid + // ---------------------------------------------------------------- + it('Should clean intersect invalid 1', () => { + const T = Type.Intersect([ + Type.Object({ x: Type.Number() }), + Type.Object({ y: Type.String() }) + ]) + const R = Value.Clean(T, { x: 1, y: 2 }) + Assert.IsEqual(R, { x: 1, y: 2 }) // types are ignored + }) + // ---------------------------------------------------------------- + // Intersect Unevaluted Properties + // ---------------------------------------------------------------- + it('Should clean intersect unevaluated properties 1', () => { + const T = Type.Intersect([ + Type.Object({ x: Type.Number() }), + Type.Object({ y: Type.String() }) + ], { + unevaluatedProperties: Type.String() + }) + const R = Value.Clean(T, { x: 1, y: 2, a: 1, b: '' }) + Assert.IsEqual(R, { x: 1, y: 2, b: '' }) + }) + // ---------------------------------------------------------------- + // Intersect Illogical + // ---------------------------------------------------------------- + it('Should clean illogical 1', () => { + const T = Type.Intersect([ + Type.Number(), + Type.Object({ x: Type.Number() }) + ]) + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) + it('Should clean illogical 2', () => { + const T = Type.Intersect([ + Type.Number(), + Type.Object({ x: Type.Number() }) + ]) + const R = Value.Clean(T, 1) + Assert.IsEqual(R, 1) + }) + it('Should clean illogical 3', () => { + const T = Type.Intersect([ + Type.Number(), + Type.Object({ x: Type.Number() }) + ]) + const R = Value.Clean(T, {}) + Assert.IsEqual(R, {}) + }) + it('Should clean illogical 4', () => { + const T = Type.Intersect([ + Type.Number(), + Type.Object({ x: Type.Number() }) + ]) + const R = Value.Clean(T, { x: 1 }) + Assert.IsEqual(R, { x: 1 }) + }) + it('Should clean illogical 5', () => { + const T = Type.Intersect([ + Type.Number(), + Type.Object({ x: Type.Number() }), + ]) + const R = Value.Clean(T, { u: null, x: 1 }) + Assert.IsEqual(R, { u: null, x: 1 }) // u retained from Number + }) +}) diff --git a/test/runtime/value/clean/iterator.ts b/test/runtime/value/clean/iterator.ts new file mode 100644 index 000000000..ec19feb07 --- /dev/null +++ b/test/runtime/value/clean/iterator.ts @@ -0,0 +1,11 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/clean/Iterator', () => { + it('Should clean 1', () => { + const T = Type.Iterator(Type.String()) + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/clean/keyof.ts b/test/runtime/value/clean/keyof.ts new file mode 100644 index 000000000..911912992 --- /dev/null +++ b/test/runtime/value/clean/keyof.ts @@ -0,0 +1,11 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/clean/KeyOf', () => { + it('Should clean 1', () => { + const T = Type.KeyOf(Type.Object({ x: Type.Number(), y: Type.Number() })) + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/clean/kind.ts b/test/runtime/value/clean/kind.ts new file mode 100644 index 000000000..8a4b836d5 --- /dev/null +++ b/test/runtime/value/clean/kind.ts @@ -0,0 +1,11 @@ +import { Value } from '@sinclair/typebox/value' +import { Type, Kind } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/clean/Kind', () => { + it('Should clean 1', () => { + const T = Type.Unsafe({ [Kind]: 'Unknown' }) + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/clean/literal.ts b/test/runtime/value/clean/literal.ts new file mode 100644 index 000000000..bf424b784 --- /dev/null +++ b/test/runtime/value/clean/literal.ts @@ -0,0 +1,11 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/clean/Literal', () => { + it('Should clean 1', () => { + const T = Type.Literal(1) + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/clean/never.ts b/test/runtime/value/clean/never.ts new file mode 100644 index 000000000..2945598e4 --- /dev/null +++ b/test/runtime/value/clean/never.ts @@ -0,0 +1,11 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/clean/Never', () => { + it('Should clean 1', () => { + const T = Type.Never() + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/clean/not.ts b/test/runtime/value/clean/not.ts new file mode 100644 index 000000000..cb60a05d0 --- /dev/null +++ b/test/runtime/value/clean/not.ts @@ -0,0 +1,11 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/clean/Not', () => { + it('Should clean 1', () => { + const T = Type.Not(Type.Any()) + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/clean/null.ts b/test/runtime/value/clean/null.ts new file mode 100644 index 000000000..a52d81a4d --- /dev/null +++ b/test/runtime/value/clean/null.ts @@ -0,0 +1,11 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/clean/Null', () => { + it('Should clean 1', () => { + const T = Type.Null() + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/clean/number.ts b/test/runtime/value/clean/number.ts new file mode 100644 index 000000000..aafd93083 --- /dev/null +++ b/test/runtime/value/clean/number.ts @@ -0,0 +1,11 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/clean/Number', () => { + it('Should clean 1', () => { + const T = Type.Number() + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/clean/object.ts b/test/runtime/value/clean/object.ts new file mode 100644 index 000000000..805665a7e --- /dev/null +++ b/test/runtime/value/clean/object.ts @@ -0,0 +1,178 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/clean/Object', () => { + // ---------------------------------------------------------------- + // Clean + // ---------------------------------------------------------------- + it('Should clean 1', () => { + const T = Type.Object({ x: Type.Number() }) + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) + it('Should clean 2', () => { + const T = Type.Object({ x: Type.Number() }) + const R = Value.Clean(T, {}) + Assert.IsEqual(R, {}) + }) + it('Should clean 3', () => { + const T = Type.Object({ x: Type.Number() }) + const R = Value.Clean(T, { x: 1 }) + Assert.IsEqual(R, { x: 1 }) + }) + it('Should clean 4', () => { + const T = Type.Object({ x: Type.Number() }) + const R = Value.Clean(T, { x: null }) + Assert.IsEqual(R, { x: null }) + }) + // ---------------------------------------------------------------- + // Nested + // ---------------------------------------------------------------- + it('Should clean nested 1', () => { + const T = Type.Object({ + x: Type.Object({ + y: Type.Number(), + }), + }) + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) + it('Should clean nested 2', () => { + const T = Type.Object({ + x: Type.Object({ + y: Type.Number(), + }), + }) + const R = Value.Clean(T, {}) + Assert.IsEqual(R, {}) + }) + it('Should clean nested 3', () => { + const T = Type.Object({ + x: Type.Object({ + y: Type.Number(), + }), + }) + const R = Value.Clean(T, { x: null }) + Assert.IsEqual(R, { x: null }) + }) + it('Should clean nested 4', () => { + const T = Type.Object({ + x: Type.Object({ + y: Type.Number(), + }), + }) + const R = Value.Clean(T, { x: { y: null } }) + Assert.IsEqual(R, { x: { y: null } }) + }) + // ---------------------------------------------------------------- + // Additional Properties + // ---------------------------------------------------------------- + it('Should clean additional properties 1', () => { + const T = Type.Object( + { + x: Type.Number(), + y: Type.Number(), + }, + { + additionalProperties: Type.String(), + }, + ) + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) + it('Should clean additional properties 2', () => { + const T = Type.Object( + { + x: Type.Number(), + y: Type.Number(), + }, + { + additionalProperties: Type.String(), + }, + ) + const R = Value.Clean(T, {}) + Assert.IsEqual(R, {}) + }) + it('Should clean additional properties 3', () => { + const T = Type.Object( + { + x: Type.Number(), + y: Type.Number(), + }, + { + additionalProperties: Type.String(), + }, + ) + const R = Value.Clean(T, { x: 1 }) + Assert.IsEqual(R, { x: 1 }) + }) + it('Should clean additional properties 4', () => { + const T = Type.Object( + { + x: Type.Number(), + y: Type.Number(), + }, + { + additionalProperties: Type.String(), + }, + ) + const R = Value.Clean(T, { x: 1, y: 2 }) + Assert.IsEqual(R, { x: 1, y: 2 }) + }) + // ---------------------------------------------------------------- + // Additional Properties Discard + // ---------------------------------------------------------------- + it('Should clean additional properties discard 1', () => { + const T = Type.Object( + { + x: Type.Number(), + y: Type.Number(), + }, + { + additionalProperties: Type.String(), + }, + ) + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) + it('Should clean additional properties discard 2', () => { + const T = Type.Object( + { + x: Type.Number(), + y: Type.Number(), + }, + { + additionalProperties: Type.String(), + }, + ) + const R = Value.Clean(T, { k: '', d: null }) + Assert.IsEqual(R, { k: '' }) + }) + it('Should clean additional properties discard 3', () => { + const T = Type.Object( + { + x: Type.Number(), + y: Type.Number(), + }, + { + additionalProperties: Type.String(), + }, + ) + const R = Value.Clean(T, { k: '', d: null, x: 1 }) + Assert.IsEqual(R, { k: '', x: 1 }) + }) + it('Should clean additional properties discard 4', () => { + const T = Type.Object( + { + x: Type.Number(), + y: Type.Number(), + }, + { + additionalProperties: Type.String(), + }, + ) + const R = Value.Clean(T, { k: '', d: null, x: 1, y: 2 }) + Assert.IsEqual(R, { k: '', x: 1, y: 2 }) + }) +}) diff --git a/test/runtime/value/clean/promise.ts b/test/runtime/value/clean/promise.ts new file mode 100644 index 000000000..0d2d7e4ff --- /dev/null +++ b/test/runtime/value/clean/promise.ts @@ -0,0 +1,11 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/clean/Promise', () => { + it('Should clean 1', () => { + const T = Type.Promise(Type.Any()) + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/clean/record.ts b/test/runtime/value/clean/record.ts new file mode 100644 index 000000000..6ef341c15 --- /dev/null +++ b/test/runtime/value/clean/record.ts @@ -0,0 +1,114 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/clean/Record', () => { + // ---------------------------------------------------------------- + // Clean + // ---------------------------------------------------------------- + it('Should clean 1', () => { + const T = Type.Record(Type.Number(), Type.String()) + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) + it('Should clean 2', () => { + const T = Type.Record(Type.Number(), Type.String()) + const R = Value.Clean(T, {}) + Assert.IsEqual(R, {}) + }) + it('Should clean 3', () => { + const T = Type.Record(Type.Number(), Type.String()) + const R = Value.Clean(T, { 0: null }) + Assert.IsEqual(R, { 0: null }) + }) + // ---------------------------------------------------------------- + // Clean Discard + // ---------------------------------------------------------------- + it('Should clean discard 1', () => { + const T = Type.Record(Type.Number(), Type.String()) + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) + it('Should clean discard 2', () => { + const T = Type.Record(Type.Number(), Type.String()) + const R = Value.Clean(T, { a: 1 }) + Assert.IsEqual(R, {}) + }) + it('Should clean discard 3', () => { + const T = Type.Record(Type.Number(), Type.String()) + const R = Value.Clean(T, { a: 1, 0: null }) + Assert.IsEqual(R, { 0: null }) + }) + // ---------------------------------------------------------------- + // Additional Properties + // ---------------------------------------------------------------- + it('Should clean additional properties 1', () => { + const T = Type.Record(Type.Number(), Type.String(), { + additionalProperties: Type.Boolean(), + }) + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) + it('Should clean additional properties 2', () => { + const T = Type.Record(Type.Number(), Type.String(), { + additionalProperties: Type.Boolean(), + }) + const R = Value.Clean(T, {}) + Assert.IsEqual(R, {}) + }) + it('Should clean additional properties 3', () => { + const T = Type.Record(Type.Number(), Type.String(), { + additionalProperties: Type.Boolean(), + }) + const R = Value.Clean(T, { 0: null }) + Assert.IsEqual(R, { 0: null }) + }) + // ---------------------------------------------------------------- + // Additional Properties Discard + // ---------------------------------------------------------------- + it('Should clean additional properties discard 1', () => { + const T = Type.Record(Type.Number(), Type.String(), { + additionalProperties: Type.Boolean(), + }) + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) + it('Should clean additional properties discard 2', () => { + const T = Type.Record(Type.Number(), Type.String(), { + additionalProperties: Type.Boolean(), + }) + const R = Value.Clean(T, { a: null }) + Assert.IsEqual(R, {}) + }) + it('Should clean additional properties discard 3', () => { + const T = Type.Record(Type.Number(), Type.String(), { + additionalProperties: Type.Boolean(), + }) + const R = Value.Clean(T, { a: null, 0: null }) + Assert.IsEqual(R, { 0: null }) + }) + // ---------------------------------------------------------------- + // Additional Properties Keep + // ---------------------------------------------------------------- + it('Should clean additional properties keep 1', () => { + const T = Type.Record(Type.Number(), Type.String(), { + additionalProperties: Type.Boolean(), + }) + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) + it('Should clean additional properties keep 2', () => { + const T = Type.Record(Type.Number(), Type.String(), { + additionalProperties: Type.Boolean(), + }) + const R = Value.Clean(T, { a: true }) + Assert.IsEqual(R, { a: true }) + }) + it('Should clean additional properties keep 3', () => { + const T = Type.Record(Type.Number(), Type.String(), { + additionalProperties: Type.Boolean(), + }) + const R = Value.Clean(T, { a: true, 0: null }) + Assert.IsEqual(R, { a: true, 0: null }) + }) +}) diff --git a/test/runtime/value/clean/recursive.ts b/test/runtime/value/clean/recursive.ts new file mode 100644 index 000000000..1a6bb6e82 --- /dev/null +++ b/test/runtime/value/clean/recursive.ts @@ -0,0 +1,112 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/clean/Recursive', () => { + // ---------------------------------------------------------------- + // Clean + // ---------------------------------------------------------------- + it('Should clean 1', () => { + const T = Type.Recursive((This) => + Type.Object({ + id: Type.String(), + nodes: Type.Array(This), + }), + ) + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) + it('Should clean 2', () => { + const T = Type.Recursive((This) => + Type.Object({ + id: Type.String(), + nodes: Type.Array(This), + }), + ) + const R = Value.Clean(T, { id: null }) + Assert.IsEqual(R, { id: null }) + }) + it('Should clean 3', () => { + const T = Type.Recursive((This) => + Type.Object({ + id: Type.String(), + nodes: Type.Array(This), + }), + ) + const R = Value.Clean(T, { id: null, nodes: null }) + Assert.IsEqual(R, { id: null, nodes: null }) + }) + it('Should clean 4', () => { + const T = Type.Recursive((This) => + Type.Object({ + id: Type.String(), + nodes: Type.Array(This), + }), + ) + const R = Value.Clean(T, { id: null, nodes: [] }) + Assert.IsEqual(R, { id: null, nodes: [] }) + }) + it('Should clean 5', () => { + const T = Type.Recursive((This) => + Type.Object({ + id: Type.String(), + nodes: Type.Array(This), + }), + ) + const R = Value.Clean(T, { id: null, nodes: [{ id: null }] }) + Assert.IsEqual(R, { id: null, nodes: [{ id: null }] }) + }) + // ---------------------------------------------------------------- + // Clean Discard + // ---------------------------------------------------------------- + it('Should clean discard 1', () => { + const T = Type.Recursive((This) => + Type.Object({ + id: Type.String(), + nodes: Type.Array(This), + }), + ) + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) + it('Should clean discard 2', () => { + const T = Type.Recursive((This) => + Type.Object({ + id: Type.String(), + nodes: Type.Array(This), + }), + ) + const R = Value.Clean(T, { u: null, id: null }) + Assert.IsEqual(R, { id: null }) + }) + it('Should clean discard 3', () => { + const T = Type.Recursive((This) => + Type.Object({ + id: Type.String(), + nodes: Type.Array(This), + }), + ) + const R = Value.Clean(T, { u: null, id: null, nodes: null }) + Assert.IsEqual(R, { id: null, nodes: null }) + }) + it('Should clean discard 4', () => { + const T = Type.Recursive((This) => + Type.Object({ + id: Type.String(), + nodes: Type.Array(This), + }), + ) + const R = Value.Clean(T, { u: null, id: null, nodes: [] }) + Assert.IsEqual(R, { id: null, nodes: [] }) + }) + it('Should clean discard 5', () => { + const T = Type.Recursive((This) => + Type.Object({ + id: Type.String(), + nodes: Type.Array(This), + }), + ) + const R = Value.Clean(T, { u: null, id: null, nodes: [{ u: null, id: null }] }) + Assert.IsEqual(R, { id: null, nodes: [{ id: null }] }) + }) +}) diff --git a/test/runtime/value/clean/ref.ts b/test/runtime/value/clean/ref.ts new file mode 100644 index 000000000..b06556159 --- /dev/null +++ b/test/runtime/value/clean/ref.ts @@ -0,0 +1,78 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/clean/Ref', () => { + // ---------------------------------------------------------------- + // Clean + // ---------------------------------------------------------------- + it('Should clean 1', () => { + const A = Type.Object( + { + x: Type.Number(), + }, + { $id: 'A' }, + ) + const T = Type.Ref('A') + const R = Value.Clean(T, [A], null) + Assert.IsEqual(R, null) + }) + it('Should clean 2', () => { + const A = Type.Object( + { + x: Type.Number(), + }, + { $id: 'A' }, + ) + const T = Type.Ref('A') + const R = Value.Clean(T, [A], {}) + Assert.IsEqual(R, {}) + }) + it('Should clean 3', () => { + const A = Type.Object( + { + x: Type.Number(), + }, + { $id: 'A' }, + ) + const T = Type.Ref('A') + const R = Value.Clean(T, [A], { x: null }) + Assert.IsEqual(R, { x: null }) + }) + // ---------------------------------------------------------------- + // Clean Discard + // ---------------------------------------------------------------- + it('Should clean discard 1', () => { + const A = Type.Object( + { + x: Type.Number(), + }, + { $id: 'A' }, + ) + const T = Type.Ref('A') + const R = Value.Clean(T, [A], null) + Assert.IsEqual(R, null) + }) + it('Should clean discard 2', () => { + const A = Type.Object( + { + x: Type.Number(), + }, + { $id: 'A' }, + ) + const T = Type.Ref('A') + const R = Value.Clean(T, [A], { a: null }) + Assert.IsEqual(R, {}) + }) + it('Should clean discard 3', () => { + const A = Type.Object( + { + x: Type.Number(), + }, + { $id: 'A' }, + ) + const T = Type.Ref('A') + const R = Value.Clean(T, [A], { a: null, x: null }) + Assert.IsEqual(R, { x: null }) + }) +}) diff --git a/test/runtime/value/clean/regexp.ts b/test/runtime/value/clean/regexp.ts new file mode 100644 index 000000000..6b1d57421 --- /dev/null +++ b/test/runtime/value/clean/regexp.ts @@ -0,0 +1,11 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/clean/RegExp', () => { + it('Should clean 1', () => { + const T = Type.RegExp('') + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/clean/string.ts b/test/runtime/value/clean/string.ts new file mode 100644 index 000000000..aa5d75727 --- /dev/null +++ b/test/runtime/value/clean/string.ts @@ -0,0 +1,11 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/clean/String', () => { + it('Should clean 1', () => { + const T = Type.String() + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/clean/symbol.ts b/test/runtime/value/clean/symbol.ts new file mode 100644 index 000000000..62047d28e --- /dev/null +++ b/test/runtime/value/clean/symbol.ts @@ -0,0 +1,11 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/clean/Symbol', () => { + it('Should clean 1', () => { + const T = Type.Symbol() + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/clean/template-literal.ts b/test/runtime/value/clean/template-literal.ts new file mode 100644 index 000000000..f76730548 --- /dev/null +++ b/test/runtime/value/clean/template-literal.ts @@ -0,0 +1,11 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/clean/TemplateLiteral', () => { + it('Should clean 1', () => { + const T = Type.TemplateLiteral('') + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/clean/tuple.ts b/test/runtime/value/clean/tuple.ts new file mode 100644 index 000000000..81618f0f9 --- /dev/null +++ b/test/runtime/value/clean/tuple.ts @@ -0,0 +1,110 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/clean/Tuple', () => { + // ---------------------------------------------------------------- + // Clean + // ---------------------------------------------------------------- + it('Should clean 1', () => { + const T = Type.Tuple([Type.Number(), Type.Number()]) + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) + it('Should clean 2', () => { + const T = Type.Tuple([Type.Number(), Type.Number()]) + const R = Value.Clean(T, []) + Assert.IsEqual(R, []) + }) + it('Should clean 3', () => { + const T = Type.Tuple([Type.Number(), Type.Number()]) + const R = Value.Clean(T, [1, 2]) + Assert.IsEqual(R, [1, 2]) + }) + it('Should clean 4', () => { + const T = Type.Tuple([Type.Number(), Type.Number()]) + const R = Value.Clean(T, [1, 2, 3]) + Assert.IsEqual(R, [1, 2]) + }) + // ---------------------------------------------------------------- + // Clean Deep + // ---------------------------------------------------------------- + it('Should clean deep 1', () => { + const T = Type.Tuple([ + Type.Number(), + Type.Object({ + x: Type.Number(), + }), + ]) + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) + it('Should clean deep 2', () => { + const T = Type.Tuple([ + Type.Number(), + Type.Object({ + x: Type.Number(), + }), + ]) + const R = Value.Clean(T, []) + Assert.IsEqual(R, []) + }) + it('Should clean deep 3', () => { + const T = Type.Tuple([ + Type.Number(), + Type.Object({ + x: Type.Number(), + }), + ]) + const R = Value.Clean(T, [1]) + Assert.IsEqual(R, [1]) + }) + it('Should clean deep 4', () => { + const T = Type.Tuple([ + Type.Number(), + Type.Object({ + x: Type.Number(), + }), + ]) + const R = Value.Clean(T, [1, null]) + Assert.IsEqual(R, [1, null]) + }) + it('Should clean deep 5', () => { + const T = Type.Tuple([ + Type.Number(), + Type.Object({ + x: Type.Number(), + }), + ]) + const R = Value.Clean(T, [1, { x: null }]) + Assert.IsEqual(R, [1, { x: null }]) + }) + it('Should clean deep 6', () => { + const T = Type.Tuple([ + Type.Number(), + Type.Object({ + x: Type.Number(), + }), + ]) + const R = Value.Clean(T, [1, { u: null, x: null }]) + Assert.IsEqual(R, [1, { x: null }]) + }) + // ---------------------------------------------------------------- + // Clean Empty + // ---------------------------------------------------------------- + it('Should clean empty 1', () => { + const T = Type.Tuple([]) + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) + it('Should clean empty 2', () => { + const T = Type.Tuple([]) + const R = Value.Clean(T, []) + Assert.IsEqual(R, []) + }) + it('Should clean empty 3', () => { + const T = Type.Tuple([]) + const R = Value.Clean(T, [1]) + Assert.IsEqual(R, []) + }) +}) diff --git a/test/runtime/value/clean/uint8array.ts b/test/runtime/value/clean/uint8array.ts new file mode 100644 index 000000000..732eb5b71 --- /dev/null +++ b/test/runtime/value/clean/uint8array.ts @@ -0,0 +1,11 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/clean/Uint8Array', () => { + it('Should clean 1', () => { + const T = Type.Uint8Array() + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/clean/undefined.ts b/test/runtime/value/clean/undefined.ts new file mode 100644 index 000000000..4b590c41b --- /dev/null +++ b/test/runtime/value/clean/undefined.ts @@ -0,0 +1,11 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/clean/Undefined', () => { + it('Should clean 1', () => { + const T = Type.Undefined() + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/clean/union.ts b/test/runtime/value/clean/union.ts new file mode 100644 index 000000000..010bd0c72 --- /dev/null +++ b/test/runtime/value/clean/union.ts @@ -0,0 +1,157 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/clean/Union', () => { + // ---------------------------------------------------------------- + // Clean + // ---------------------------------------------------------------- + it('Should clean 1', () => { + const T = Type.Union([Type.Number(), Type.Boolean()]) + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) + it('Should clean 2', () => { + const T = Type.Union([Type.Number(), Type.Boolean()]) + const R = Value.Clean(T, 1) + Assert.IsEqual(R, 1) + }) + it('Should clean 2', () => { + const T = Type.Union([Type.Number(), Type.Boolean()]) + const R = Value.Clean(T, true) + Assert.IsEqual(R, true) + }) + // ---------------------------------------------------------------- + // Clean Select + // ---------------------------------------------------------------- + it('Should clean select 1', () => { + const X = Type.Object({ x: Type.Number() }) + const Y = Type.Object({ y: Type.Number() }) + const T = Type.Union([X, Y]) + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) + it('Should clean select 2', () => { + const X = Type.Object({ x: Type.Number() }) + const Y = Type.Object({ y: Type.Number() }) + const T = Type.Union([X, Y]) + const R = Value.Clean(T, {}) + Assert.IsEqual(R, {}) + }) + it('Should clean select 3', () => { + const X = Type.Object({ x: Type.Number() }) + const Y = Type.Object({ y: Type.Number() }) + const T = Type.Union([X, Y]) + const R = Value.Clean(T, { x: null }) + Assert.IsEqual(R, { x: null }) + }) + it('Should clean select 4', () => { + const X = Type.Object({ x: Type.Number() }) + const Y = Type.Object({ y: Type.Number() }) + const T = Type.Union([X, Y]) + const R = Value.Clean(T, { y: null }) + Assert.IsEqual(R, { y: null }) + }) + // ---------------------------------------------------------------- + // Clean Select Discard + // ---------------------------------------------------------------- + it('Should clean select discard 1', () => { + const X = Type.Object({ x: Type.Number() }) + const Y = Type.Object({ y: Type.Number() }) + const T = Type.Union([X, Y]) + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) + it('Should clean select discard 2', () => { + const X = Type.Object({ x: Type.Number() }) + const Y = Type.Object({ y: Type.Number() }) + const T = Type.Union([X, Y]) + const R = Value.Clean(T, { u: null }) + Assert.IsEqual(R, { u: null }) // no match + }) + it('Should clean select discard 3', () => { + const X = Type.Object({ x: Type.Number() }) + const Y = Type.Object({ y: Type.Number() }) + const T = Type.Union([X, Y]) + const R = Value.Clean(T, { u: null, x: 1 }) + Assert.IsEqual(R, { x: 1 }) + }) + it('Should clean select discard 4', () => { + const X = Type.Object({ x: Type.Number() }) + const Y = Type.Object({ y: Type.Number() }) + const T = Type.Union([X, Y]) + const R = Value.Clean(T, { u: null, y: 1 }) + Assert.IsEqual(R, { y: 1 }) + }) + // ---------------------------------------------------------------- + // Clean Select Retain + // ---------------------------------------------------------------- + it('Should clean select retain 1', () => { + const X = Type.Object({ x: Type.Number() }, { additionalProperties: Type.Null() }) + const Y = Type.Object({ y: Type.Number() }, { additionalProperties: Type.Null() }) + const T = Type.Union([X, Y]) + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) + it('Should clean select retain 2', () => { + const X = Type.Object({ x: Type.Number() }, { additionalProperties: Type.Null() }) + const Y = Type.Object({ y: Type.Number() }, { additionalProperties: Type.Null() }) + const T = Type.Union([X, Y]) + const R = Value.Clean(T, { u: null }) + Assert.IsEqual(R, { u: null }) + }) + it('Should clean select retain 3', () => { + const X = Type.Object({ x: Type.Number() }, { additionalProperties: Type.Null() }) + const Y = Type.Object({ y: Type.Number() }, { additionalProperties: Type.Null() }) + const T = Type.Union([X, Y]) + const R = Value.Clean(T, { u: null, x: 1 }) + Assert.IsEqual(R, { u: null, x: 1 }) + }) + it('Should clean select retain 4', () => { + const X = Type.Object({ x: Type.Number() }, { additionalProperties: Type.Null() }) + const Y = Type.Object({ y: Type.Number() }, { additionalProperties: Type.Null() }) + const T = Type.Union([X, Y]) + const R = Value.Clean(T, { u: null, y: 1 }) + Assert.IsEqual(R, { u: null, y: 1 }) + }) + // ---------------------------------------------------------------- + // Clean Select First and Discard + // ---------------------------------------------------------------- + it('Should clean select first and discard 1', () => { + const X = Type.Object({ x: Type.Number() }) + const Y = Type.Object({ y: Type.Number() }, { additionalProperties: Type.Null() }) + const T = Type.Union([X, Y]) + const R = Value.Clean(T, { u: null, x: 1 }) + Assert.IsEqual(R, { x: 1 }) + }) + it('Should clean select first and discard 2', () => { + const X = Type.Object({ x: Type.Number() }) + const Y = Type.Object({ y: Type.Number() }, { additionalProperties: Type.Null() }) + const T = Type.Union([X, Y]) + const R = Value.Clean(T, { u: null, y: 1 }) + Assert.IsEqual(R, { u: null, y: 1 }) + }) + // ---------------------------------------------------------------- + // Union Recursive + // + // https://github.com/sinclairzx81/typebox/issues/845 + // ---------------------------------------------------------------- + it('Should clean recursive with union', () => { + const T = Type.Recursive((This) => + Type.Object({ + id: Type.Number(), + parent: Type.Union([This, Type.Null()]), + }), + ) + const R = Value.Clean(T, { + id: 1, + unknown: 1, + parent: { + id: 2, + unknown: 1, + parent: null, + }, + }) + Assert.IsEqual(R, { id: 1, parent: { id: 2, parent: null } }) + }) +}) diff --git a/test/runtime/value/clean/unknown.ts b/test/runtime/value/clean/unknown.ts new file mode 100644 index 000000000..edba3ebfc --- /dev/null +++ b/test/runtime/value/clean/unknown.ts @@ -0,0 +1,11 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/clean/Unknown', () => { + it('Should clean 1', () => { + const T = Type.Unknown() + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/clean/void.ts b/test/runtime/value/clean/void.ts new file mode 100644 index 000000000..cd4c20db0 --- /dev/null +++ b/test/runtime/value/clean/void.ts @@ -0,0 +1,11 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/clean/Void', () => { + it('Should clean 1', () => { + const T = Type.Void() + const R = Value.Clean(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/clone/clone.ts b/test/runtime/value/clone/clone.ts new file mode 100644 index 000000000..15ecb1f57 --- /dev/null +++ b/test/runtime/value/clone/clone.ts @@ -0,0 +1,156 @@ +import { Value } from '@sinclair/typebox/value' +import { Assert } from '../../assert/index' + +describe('value/clone/Clone', () => { + // -------------------------------------------- + // ValueType + // -------------------------------------------- + it('Should clone null', () => { + const R = Value.Clone(null) + Assert.IsEqual(R, null) + }) + it('Should clone undefined', () => { + const R = Value.Clone(undefined) + Assert.IsEqual(R, undefined) + }) + it('Should clone number', () => { + const R = Value.Clone(1) + Assert.IsEqual(R, 1) + }) + it('Should clone bigint', () => { + const R = Value.Clone(1n) + Assert.IsEqual(R, 1n) + }) + it('Should clone boolean', () => { + const R = Value.Clone(true) + Assert.IsEqual(R, true) + }) + it('Should clone string', () => { + const R = Value.Clone('hello') + Assert.IsEqual(R, 'hello') + }) + it('Should clone symbol', () => { + const S = Symbol('hello') + const R = Value.Clone(S) + Assert.IsEqual(R, S) + }) + // -------------------------------------------- + // ObjectType + // -------------------------------------------- + it('Should clone object #1', () => { + const V = { + x: 1, + y: 2, + z: 3, + } + const R = Value.Clone(V) + Assert.IsEqual(R, V) + }) + it('Should clone object #2', () => { + const V = { + x: 1, + y: 2, + z: 3, + w: { + a: 1, + b: 2, + c: 3, + }, + } + const R = Value.Clone(V) + Assert.IsEqual(R, V) + }) + it('Should clone object #3', () => { + const V = { + x: 1, + y: 2, + z: 3, + w: [0, 1, 2, 3, 4], + } + const R = Value.Clone(V) + Assert.IsEqual(R, V) + }) + // -------------------------------------------- + // ArrayType + // -------------------------------------------- + it('Should clone array #1', () => { + const V = [1, 2, 3, 4] + const R = Value.Clone(V) + Assert.IsEqual(R, V) + }) + it('Should clone array #2', () => { + const V = [ + [1, 2, 3], + [1, 2, 3], + [1, 2, 3], + [1, 2, 3], + ] + const R = Value.Clone(V) + Assert.IsEqual(R, V) + }) + it('Should clone array #3', () => { + const V = [ + { x: 1, y: 2, z: 3 }, + { x: 1, y: 2, z: 3 }, + { x: 1, y: 2, z: 3 }, + { x: 1, y: 2, z: 3 }, + ] + const R = Value.Clone(V) + Assert.IsEqual(R, V) + }) + it('Should clone Int8Array', () => { + const V = new Int8Array([1, 2, 3, 4]) + const R = Value.Clone(V) + Assert.IsEqual(R, V) + }) + it('Should clone Uint8Array', () => { + const V = new Uint8Array([1, 2, 3, 4]) + const R = Value.Clone(V) + Assert.IsEqual(R, V) + }) + it('Should clone Uint8ClampedArray', () => { + const V = new Uint8ClampedArray([1, 2, 3, 4]) + const R = Value.Clone(V) + Assert.IsEqual(R, V) + }) + it('Should clone Int16Array', () => { + const V = new Int16Array([1, 2, 3, 4]) + const R = Value.Clone(V) + Assert.IsEqual(R, V) + }) + it('Should clone Uint16Array', () => { + const V = new Uint16Array([1, 2, 3, 4]) + const R = Value.Clone(V) + Assert.IsEqual(R, V) + }) + it('Should clone Int32Array', () => { + const V = new Int32Array([1, 2, 3, 4]) + const R = Value.Clone(V) + Assert.IsEqual(R, V) + }) + it('Should clone Uint32Array', () => { + const V = new Int32Array([1, 2, 3, 4]) + const R = Value.Clone(V) + Assert.IsEqual(R, V) + }) + it('Should clone Float32Array', () => { + const V = new Float32Array([1, 2, 3, 4]) + const R = Value.Clone(V) + Assert.IsEqual(R, V) + }) + it('Should clone Float64Array', () => { + const V = new Float64Array([1, 2, 3, 4]) + const R = Value.Clone(V) + Assert.IsEqual(R, V) + }) + it('Should clone BigInt64Array', () => { + const V = new BigInt64Array([1n, 2n, 3n, 4n]) + const R = Value.Clone(V) + Assert.IsEqual(R, V) + }) + it('Should clone BigUint64Array', () => { + const V = new BigUint64Array([1n, 2n, 3n, 4n]) + const R = Value.Clone(V) + Assert.IsEqual(R, V) + }) +}) diff --git a/test/runtime/value/clone/index.ts b/test/runtime/value/clone/index.ts new file mode 100644 index 000000000..cd798158e --- /dev/null +++ b/test/runtime/value/clone/index.ts @@ -0,0 +1 @@ +import './clone' diff --git a/test/runtime/value/convert/any.ts b/test/runtime/value/convert/any.ts new file mode 100644 index 000000000..96a6d5841 --- /dev/null +++ b/test/runtime/value/convert/any.ts @@ -0,0 +1,42 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/convert/Any', () => { + const T = Type.Any() + it('Should convert null', () => { + const V = null + const R = Value.Convert(T, V) + Assert.IsEqual(R, V) + }) + it('Should convert undefined', () => { + const V = undefined + const R = Value.Convert(T, V) + Assert.IsEqual(R, V) + }) + it('Should convert string', () => { + const V = 'hello' + const R = Value.Convert(T, V) + Assert.IsEqual(R, V) + }) + it('Should convert number', () => { + const V = 42 + const R = Value.Convert(T, V) + Assert.IsEqual(R, V) + }) + it('Should convert boolean', () => { + const V = true + const R = Value.Convert(T, V) + Assert.IsEqual(R, V) + }) + it('Should convert object', () => { + const V = { x: 1 } + const R = Value.Convert(T, V) + Assert.IsEqual(R, V) + }) + it('Should convert array', () => { + const V = [1, 2, 3] + const R = Value.Convert(T, V) + Assert.IsEqual(R, V) + }) +}) diff --git a/test/runtime/value/convert/array.ts b/test/runtime/value/convert/array.ts new file mode 100644 index 000000000..345a5564a --- /dev/null +++ b/test/runtime/value/convert/array.ts @@ -0,0 +1,56 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/convert/Array', () => { + it('Should convert array of number', () => { + const T = Type.Array(Type.Number()) + const R = Value.Convert(T, [1, 3.14, '1', '3.14', true, false, 'true', 'false', 'hello']) + Assert.IsEqual(R, [1, 3.14, 1, 3.14, 1, 0, 1, 0, 'hello']) + }) + it('Should convert array of boolean', () => { + const T = Type.Array(Type.Boolean()) + const R = Value.Convert(T, [1, 3.14, '1', '3.14', true, false, 'true', 'false', 'hello']) + Assert.IsEqual(R, [true, 3.14, true, '3.14', true, false, true, false, 'hello']) + }) + it('Should convert array of string', () => { + const T = Type.Array(Type.String()) + const R = Value.Convert(T, [1, 3.14, '1', '3.14', true, false, 'true', 'false', 'hello']) + Assert.IsEqual(R, ['1', '3.14', '1', '3.14', 'true', 'false', 'true', 'false', 'hello']) + }) + it('Should convert array of date', () => { + const T = Type.Array(Type.Date()) + const R = Value.Convert(T, [1, '1', true, false, 'true', 'false', 'hello']) as any[] + Assert.IsEqual(R[0].getTime(), 1) + Assert.IsEqual(R[1].getTime(), 1) + Assert.IsEqual(R[2].getTime(), 1) + Assert.IsEqual(R[3].getTime(), 0) + Assert.IsEqual(R[4].getTime(), 1) + Assert.IsEqual(R[5].getTime(), 0) + Assert.IsEqual(R[6], 'hello') + }) + // ---------------------------------------------------------------- + // Array Coercion + // ---------------------------------------------------------------- + it('Should convert into array (convert interior)', () => { + const T = Type.Array(Type.Number()) + const R = Value.Convert(T, '1') + Assert.IsEqual(R, [1]) + }) + it('Should convert into array (retain interior)', () => { + const T = Type.Array(Type.Number()) + const R = Value.Convert(T, 'A') + Assert.IsEqual(R, ['A']) + }) + it('Should convert into array (object)', () => { + const T = Type.Array( + Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }), + ) + const R = Value.Convert(T, { x: '1', y: true, z: null }) + Assert.IsEqual(R, [{ x: 1, y: 1, z: null }]) + }) +}) diff --git a/test/runtime/value/convert/async-iterator.ts b/test/runtime/value/convert/async-iterator.ts new file mode 100644 index 000000000..bec76a3d7 --- /dev/null +++ b/test/runtime/value/convert/async-iterator.ts @@ -0,0 +1,17 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/convert/AsyncIterator', () => { + const T = Type.AsyncIterator(Type.Any()) + it('Should passthrough 1', () => { + const V = (async function* () {})() + const R = Value.Convert(T, V) + Assert.IsEqual(R, V) + }) + it('Should passthrough 2', () => { + const V = 1 + const R = Value.Convert(T, V) + Assert.IsEqual(R, V) + }) +}) diff --git a/test/runtime/value/convert/bigint.ts b/test/runtime/value/convert/bigint.ts new file mode 100644 index 000000000..02014a86e --- /dev/null +++ b/test/runtime/value/convert/bigint.ts @@ -0,0 +1,86 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/convert/BigInt', () => { + it('Should convert bigint from string 1', () => { + const T = Type.BigInt() + const R = Value.Convert(T, '1') + Assert.IsEqual(R, BigInt(1)) + }) + it('Should convert bigint from string 2', () => { + const T = Type.BigInt() + const R = Value.Convert(T, '3.14') + Assert.IsEqual(R, BigInt(3)) + }) + it('Should convert bigint from string 3', () => { + const T = Type.BigInt() + const R = Value.Convert(T, 'true') + Assert.IsEqual(R, BigInt(1)) + }) + it('Should convert bigint from string 4', () => { + const T = Type.BigInt() + const R = Value.Convert(T, 'false') + Assert.IsEqual(R, BigInt(0)) + }) + it('Should convert bigint from string 5', () => { + const T = Type.BigInt() + const R = Value.Convert(T, '12345678901234567890') + Assert.IsEqual(R, BigInt('12345678901234567890')) + }) + it('Should convert bigint from string 6', () => { + const T = Type.BigInt() + const R = Value.Convert(T, '-12345678901234567890') + Assert.IsEqual(R, BigInt('-12345678901234567890')) + }) + it('Should convert bigint from string 7', () => { + const T = Type.BigInt() + const R = Value.Convert(T, '12345678901234567890.123') + Assert.IsEqual(R, BigInt('12345678901234567890')) + }) + it('Should convert bigint from string 8', () => { + const T = Type.BigInt() + const R = Value.Convert(T, '-12345678901234567890.123') + Assert.IsEqual(R, BigInt('-12345678901234567890')) + }) + it('Should convert bigint from number 1', () => { + const T = Type.BigInt() + const R = Value.Convert(T, 1) + Assert.IsEqual(R, BigInt(1)) + }) + it('Should convert bigint from number 2', () => { + const T = Type.BigInt() + const R = Value.Convert(T, 3.14) + Assert.IsEqual(R, BigInt(3)) + }) + it('Should convert bigint from number 3', () => { + const T = Type.BigInt() + const R = Value.Convert(T, Math.pow(2, 31)) + Assert.IsEqual(R, BigInt(2147483648)) + }) + it('Should convert bigint from number 4', () => { + const T = Type.BigInt() + const R = Value.Convert(T, Number.MAX_SAFE_INTEGER) + Assert.IsEqual(R, BigInt(9007199254740991)) + }) + it('Should convert bigint from number 5', () => { + const T = Type.BigInt() + const R = Value.Convert(T, 123456789012345.6789) + Assert.IsEqual(R, BigInt(123456789012345)) + }) + it('Should convert bigint from number 6', () => { + const T = Type.BigInt() + const R = Value.Convert(T, -123456789012345.6789) + Assert.IsEqual(R, BigInt(-123456789012345)) + }) + it('Should convert bigint from boolean 1', () => { + const T = Type.BigInt() + const R = Value.Convert(T, true) + Assert.IsEqual(R, BigInt(1)) + }) + it('Should convert bigint from boolean 2', () => { + const T = Type.BigInt() + const R = Value.Convert(T, false) + Assert.IsEqual(R, BigInt(0)) + }) +}) diff --git a/test/runtime/value/convert/boolean.ts b/test/runtime/value/convert/boolean.ts new file mode 100644 index 000000000..8e91f6d5f --- /dev/null +++ b/test/runtime/value/convert/boolean.ts @@ -0,0 +1,154 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/convert/Boolean', () => { + it('Should convert from string 1', () => { + const T = Type.Boolean() + const R = Value.Convert(T, '1') + Assert.IsEqual(R, true) + }) + it('Should convert from string 2', () => { + const T = Type.Boolean() + const R = Value.Convert(T, '3.14') + Assert.IsEqual(R, '3.14') + }) + it('Should convert from string 3', () => { + const T = Type.Boolean() + const R = Value.Convert(T, 'true') + Assert.IsEqual(R, true) + }) + it('Should convert from string 4', () => { + const T = Type.Boolean() + const R = Value.Convert(T, 'false') + Assert.IsEqual(R, false) + }) + it('Should convert from number 1', () => { + const T = Type.Boolean() + const R = Value.Convert(T, 1) + Assert.IsEqual(R, true) + }) + it('Should convert from number 2', () => { + const T = Type.Boolean() + const R = Value.Convert(T, 3.14) + Assert.IsEqual(R, 3.14) + }) + it('Should convert from number 3', () => { + const T = Type.Boolean() + const R = Value.Convert(T, 1.1) + Assert.IsEqual(R, 1.1) + }) + it('Should convert from boolean 1', () => { + const T = Type.Boolean() + const R = Value.Convert(T, true) + Assert.IsEqual(R, true) + }) + it('Should convert from boolean 2', () => { + const T = Type.Boolean() + const R = Value.Convert(T, false) + Assert.IsEqual(R, false) + }) + // ---------------------------------------------------------- + // Casts + // ---------------------------------------------------------- + it('Should convert string #1', () => { + const value = 'hello' + const result = Value.Convert(Type.Boolean(), value) + Assert.IsEqual(result, 'hello') + }) + it('Should convert string #2', () => { + const value = 'true' + const result = Value.Convert(Type.Boolean(), value) + Assert.IsEqual(result, true) + }) + it('Should convert string #3', () => { + const value = 'TRUE' + const result = Value.Convert(Type.Boolean(), value) + Assert.IsEqual(result, true) + }) + it('Should convert string #4', () => { + const value = 'false' + const result = Value.Convert(Type.Boolean(), value) + Assert.IsEqual(result, false) + }) + it('Should convert string #5', () => { + const value = '0' + const result = Value.Convert(Type.Boolean(), value) + Assert.IsEqual(result, false) + }) + it('Should convert string #6', () => { + const value = '1' + const result = Value.Convert(Type.Boolean(), value) + Assert.IsEqual(result, true) + }) + it('Should convert string #7', () => { + const value = '0' + const result = Value.Convert(Type.Boolean({ default: true }), value) + Assert.IsEqual(result, false) + }) + it('Should convert string #8', () => { + const value = '1' + const result = Value.Convert(Type.Boolean({ default: false }), value) + Assert.IsEqual(result, true) + }) + it('Should convert string #8', () => { + const value = '2' + const result = Value.Convert(Type.Boolean({ default: true }), value) + Assert.IsEqual(result, '2') + }) + it('Should convert number #1', () => { + const value = 0 + const result = Value.Convert(Type.Boolean(), value) + Assert.IsEqual(result, false) + }) + it('Should convert number #2', () => { + const value = 1n + const result = Value.Convert(Type.Boolean(), value) + Assert.IsEqual(result, true) + }) + it('Should convert number #3', () => { + const value = 1 + const result = Value.Convert(Type.Boolean(), value) + Assert.IsEqual(result, true) + }) + it('Should convert number #4', () => { + const value = 2 + const result = Value.Convert(Type.Boolean(), value) + Assert.IsEqual(result, 2) + }) + it('Should convert number #5', () => { + const value = 0 + const result = Value.Convert(Type.Boolean({ default: true }), value) + Assert.IsEqual(result, false) + }) + it('Should convert number #6', () => { + const value = 1 + const result = Value.Convert(Type.Boolean({ default: false }), value) + Assert.IsEqual(result, true) + }) + it('Should convert number #7', () => { + const value = 2 + const result = Value.Convert(Type.Boolean({ default: true }), value) + Assert.IsEqual(result, 2) + }) + it('Should convert true', () => { + const value = true + const result = Value.Convert(Type.Boolean(), value) + Assert.IsEqual(result, true) + }) + it('Should convert false', () => { + const value = false + const result = Value.Convert(Type.Boolean(), value) + Assert.IsEqual(result, false) + }) + it('Should convert object', () => { + const value = {} + const result = Value.Convert(Type.Boolean(), value) + Assert.IsEqual(result, {}) + }) + it('Should convert array', () => { + const value = [] as any[] + const result = Value.Convert(Type.Boolean(), value) + Assert.IsEqual(result, []) + }) +}) diff --git a/test/runtime/value/convert/composite.ts b/test/runtime/value/convert/composite.ts new file mode 100644 index 000000000..7114db8bd --- /dev/null +++ b/test/runtime/value/convert/composite.ts @@ -0,0 +1,16 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/convert/Composite', () => { + it('Should convert properties', () => { + // prettier-ignore + const T = Type.Composite([ + Type.Object({ x: Type.Number() }), + Type.Object({ y: Type.Boolean() }), + Type.Object({ z: Type.Boolean() }) + ]) + const R = Value.Convert(T, { x: '42', y: 'true', z: 'hello' }) + Assert.IsEqual(R, { x: 42, y: true, z: 'hello' }) + }) +}) diff --git a/test/runtime/value/convert/constructor.ts b/test/runtime/value/convert/constructor.ts new file mode 100644 index 000000000..dabd0a2fb --- /dev/null +++ b/test/runtime/value/convert/constructor.ts @@ -0,0 +1,17 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/convert/Constructor', () => { + const T = Type.Constructor([], Type.Any()) + it('Should passthrough 1', () => { + const V = class {} + const R = Value.Convert(T, V) + Assert.IsEqual(R, V) + }) + it('Should passthrough 2', () => { + const V = 1 + const R = Value.Convert(T, V) + Assert.IsEqual(R, V) + }) +}) diff --git a/test/runtime/value/convert/date.ts b/test/runtime/value/convert/date.ts new file mode 100644 index 000000000..abe6066a8 --- /dev/null +++ b/test/runtime/value/convert/date.ts @@ -0,0 +1,42 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/convert/Date', () => { + it('Should convert from number', () => { + const result = Value.Convert(Type.Date(), 123) as Date + Assert.IsEqual(result.getTime(), 123) + }) + it('Should convert from numeric string', () => { + const result = Value.Convert(Type.Date(), '123') as Date + Assert.IsEqual(result.getTime(), 123) + }) + it('Should convert from boolean true (interpretted as numeric 1)', () => { + const result = Value.Convert(Type.Date(), true) as Date + Assert.IsEqual(result.getTime(), 1) + }) + it('Should convert from datetime string', () => { + const result = Value.Convert(Type.Date(), '1980-02-03T01:02:03.000Z') as Date + Assert.IsEqual(result.toISOString(), '1980-02-03T01:02:03.000Z') + }) + it('Should convert from datetime string without timezone', () => { + const result = Value.Convert(Type.Date(), '1980-02-03T01:02:03') as Date + Assert.IsEqual(result.toISOString(), '1980-02-03T01:02:03.000Z') + }) + it('Should convert from time with timezone', () => { + const result = Value.Convert(Type.Date(), '01:02:03.000Z') as Date + Assert.IsEqual(result.toISOString(), '1970-01-01T01:02:03.000Z') + }) + it('Should convert from time without timezone', () => { + const result = Value.Convert(Type.Date(), '01:02:03') as Date + Assert.IsEqual(result.toISOString(), '1970-01-01T01:02:03.000Z') + }) + it('Should convert from date string', () => { + const result = Value.Convert(Type.Date(), '1980-02-03') as Date + Assert.IsEqual(result.toISOString(), '1980-02-03T00:00:00.000Z') + }) + it('Should convert invalid strings to unix epoch 0', () => { + const result = Value.Convert(Type.Date(), 'invalid-date') as Date + Assert.IsEqual(result, 'invalid-date') + }) +}) diff --git a/test/runtime/value/convert/enum.ts b/test/runtime/value/convert/enum.ts new file mode 100644 index 000000000..a42c758e9 --- /dev/null +++ b/test/runtime/value/convert/enum.ts @@ -0,0 +1,7 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/convert/Enum', () => { + it('Should convert', () => {}) +}) diff --git a/test/runtime/value/convert/function.ts b/test/runtime/value/convert/function.ts new file mode 100644 index 000000000..74e72dd7f --- /dev/null +++ b/test/runtime/value/convert/function.ts @@ -0,0 +1,17 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/convert/Function', () => { + const T = Type.Function([], Type.Any()) + it('Should passthrough 1', () => { + const V = function () {} + const R = Value.Convert(T, V) + Assert.IsEqual(R, V) + }) + it('Should passthrough 2', () => { + const V = 1 + const R = Value.Convert(T, V) + Assert.IsEqual(R, V) + }) +}) diff --git a/test/runtime/value/convert/import.ts b/test/runtime/value/convert/import.ts new file mode 100644 index 000000000..f22c506b2 --- /dev/null +++ b/test/runtime/value/convert/import.ts @@ -0,0 +1,29 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +// prettier-ignore +describe('value/convert/Import', () => { + it('Should convert properties', () => { + const T = Type.Module({ A: Type.Object({ + x: Type.Number(), + y: Type.Boolean(), + z: Type.Boolean() + })}).Import('A') + const R = Value.Convert(T, { x: '42', y: 'true', z: 'hello' }) + Assert.IsEqual(R, { x: 42, y: true, z: 'hello' }) + }) + it('Should convert known properties', () => { + const T = Type.Module({ A: Type.Object({ + x: Type.Number(), + y: Type.Boolean() + })}).Import('A') + const R = Value.Convert(T, { x: '42', y: 'true', z: 'hello' }) + Assert.IsEqual(R, { x: 42, y: true, z: 'hello' }) + }) + it('Should not convert missing properties', () => { + const T = Type.Module({ A: Type.Object({ x: Type.Number() }) }).Import('A') + const R = Value.Convert(T, { }) + Assert.IsEqual(R, { }) + }) +}) diff --git a/test/runtime/value/convert/index.ts b/test/runtime/value/convert/index.ts new file mode 100644 index 000000000..fe5f47fd7 --- /dev/null +++ b/test/runtime/value/convert/index.ts @@ -0,0 +1,33 @@ +import './any' +import './array' +import './async-iterator' +import './bigint' +import './boolean' +import './composite' +import './constructor' +import './kind' +import './date' +import './enum' +import './function' +import './import' +import './integer' +import './intersect' +import './iterator' +import './keyof' +import './literal' +import './never' +import './null' +import './number' +import './object' +import './promise' +import './recursive' +import './record' +import './regexp' +import './string' +import './symbol' +import './tuple' +import './uint8array' +import './undefined' +import './union' +import './unknown' +import './void' diff --git a/test/runtime/value/convert/integer.ts b/test/runtime/value/convert/integer.ts new file mode 100644 index 000000000..abfc59792 --- /dev/null +++ b/test/runtime/value/convert/integer.ts @@ -0,0 +1,102 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/convert/Integer', () => { + it('Should convert from string 1', () => { + const T = Type.Integer() + const R = Value.Convert(T, '3.14') + Assert.IsEqual(R, 3) + }) + it('Should convert from string 2', () => { + const T = Type.Integer() + const R = Value.Convert(T, '42') + Assert.IsEqual(R, 42) + }) + it('Should convert from boolean 1', () => { + const T = Type.Integer() + const R = Value.Convert(T, true) + Assert.IsEqual(R, 1) + }) + it('Should convert from boolean 2', () => { + const T = Type.Integer() + const R = Value.Convert(T, false) + Assert.IsEqual(R, 0) + }) + it('Should convert from number 1', () => { + const T = Type.Integer() + const R = Value.Convert(T, 3.14) + Assert.IsEqual(R, 3) + }) + // ---------------------------------------------------------- + // Casts + // ---------------------------------------------------------- + it('Should convert string #1', () => { + const value = 'hello' + const result = Value.Convert(Type.Integer(), value) + Assert.IsEqual(result, 'hello') + }) + it('Should convert string #2', () => { + const value = '3.14' + const result = Value.Convert(Type.Integer(), value) + Assert.IsEqual(result, 3) + }) + it('Should convert string #3', () => { + const value = '-0' + const result = Value.Convert(Type.Integer(), value) + Assert.IsEqual(result, -0) + }) + it('Should convert string #4', () => { + const value = '-100' + const result = Value.Convert(Type.Integer(), value) + Assert.IsEqual(result, -100) + }) + it('Should convert number', () => { + const value = 42 + const result = Value.Convert(Type.Integer(), value) + Assert.IsEqual(result, 42) + }) + it('Should convert true', () => { + const value = true + const result = Value.Convert(Type.Integer(), value) + Assert.IsEqual(result, 1) + }) + it('Should convert false', () => { + const value = false + const result = Value.Convert(Type.Integer(), value) + Assert.IsEqual(result, 0) + }) + it('Should convert object', () => { + const value = {} + const result = Value.Convert(Type.Integer(), value) + Assert.IsEqual(result, {}) + }) + it('Should convert array', () => { + const value = [] as any[] + const result = Value.Convert(Type.Integer(), value) + Assert.IsEqual(result, []) + }) + // ---------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/1147 + // ---------------------------------------------------------- + it('Should convert large Integer 1', () => { + const N = 1738213389080 + const R = Value.Convert(Type.Integer(), N) + Assert.IsEqual(R, N) + }) + it('Should convert large Integer 2', () => { + const N = 1738213389080.5555 + const R = Value.Convert(Type.Integer(), N) + Assert.IsEqual(R, 1738213389080) + }) + it('Should convert large Integer 3', () => { + const N = '1738213389080' + const R = Value.Convert(Type.Integer(), N) + Assert.IsEqual(R, 1738213389080) + }) + it('Should convert large Integer 3', () => { + const N = '1738213389080.555' + const R = Value.Convert(Type.Integer(), N) + Assert.IsEqual(R, 1738213389080) + }) +}) diff --git a/test/runtime/value/convert/intersect.ts b/test/runtime/value/convert/intersect.ts new file mode 100644 index 000000000..191150a3b --- /dev/null +++ b/test/runtime/value/convert/intersect.ts @@ -0,0 +1,48 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/convert/Intersect', () => { + it('Should convert intersected objects', () => { + // prettier-ignore + const T = Type.Intersect([ + Type.Object({ x: Type.Number() }), + Type.Object({ y: Type.Number() }) + ]) + const R = Value.Convert(T, { x: '1', y: '2' }) + Assert.IsEqual(R, { x: 1, y: 2 }) + }) + // ---------------------------------------------------------------- + // Intersection Complex + // ---------------------------------------------------------------- + it('Should complex intersect 1', () => { + // prettier-ignore + const T = Type.Intersect([ + Type.Number(), + Type.Object({ x: Type.Number() }), + Type.Object({ y: Type.Number() }) + ]) + const R = Value.Convert(T, { x: '1', y: '2' }) + Assert.IsEqual(R, { x: 1, y: 2 }) + }) + it('Should complex intersect 2', () => { + // prettier-ignore + const T = Type.Intersect([ + Type.Object({ x: Type.Number() }), + Type.Object({ y: Type.Number() }), + Type.Number(), + ]) + const R = Value.Convert(T, { x: '3', y: '4' }) + Assert.IsEqual(R, { x: 3, y: 4 }) + }) + it('Should complex intersect 3', () => { + // prettier-ignore + const T = Type.Intersect([ + Type.Number(), + Type.Object({ x: Type.Number() }), + Type.Object({ y: Type.Number() }) + ]) + const R = Value.Convert(T, '123') + Assert.IsEqual(R, 123) + }) +}) diff --git a/test/runtime/value/convert/iterator.ts b/test/runtime/value/convert/iterator.ts new file mode 100644 index 000000000..1dbf33702 --- /dev/null +++ b/test/runtime/value/convert/iterator.ts @@ -0,0 +1,17 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/convert/Iterator', () => { + const T = Type.Iterator(Type.Any()) + it('Should passthrough 1', () => { + const V = (function* () {})() + const R = Value.Convert(T, V) + Assert.IsEqual(R, V) + }) + it('Should passthrough 2', () => { + const V = 1 + const R = Value.Convert(T, V) + Assert.IsEqual(R, V) + }) +}) diff --git a/test/runtime/value/convert/keyof.ts b/test/runtime/value/convert/keyof.ts new file mode 100644 index 000000000..5f4141fe1 --- /dev/null +++ b/test/runtime/value/convert/keyof.ts @@ -0,0 +1,7 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/convert/KeyOf', () => { + it('Should convert', () => {}) +}) diff --git a/test/runtime/value/convert/kind.ts b/test/runtime/value/convert/kind.ts new file mode 100644 index 000000000..116178c9b --- /dev/null +++ b/test/runtime/value/convert/kind.ts @@ -0,0 +1,39 @@ +import { Value } from '@sinclair/typebox/value' +import { TypeRegistry, Kind, TSchema } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/convert/Kind', () => { + // --------------------------------------------------------- + // Fixtures + // --------------------------------------------------------- + beforeEach(() => TypeRegistry.Set('Kind', () => true)) + afterEach(() => TypeRegistry.Delete('Kind')) + // --------------------------------------------------------- + // Test + // --------------------------------------------------------- + it('Should not convert value 1', () => { + const T = { [Kind]: 'Kind' } as TSchema + const R = Value.Convert(T, true) + Assert.IsEqual(R, true) + }) + it('Should not convert value 2', () => { + const T = { [Kind]: 'Kind' } as TSchema + const R = Value.Convert(T, 42) + Assert.IsEqual(R, 42) + }) + it('Should not convert value 3', () => { + const T = { [Kind]: 'Kind' } as TSchema + const R = Value.Convert(T, 'hello') + Assert.IsEqual(R, 'hello') + }) + it('Should not convert value 4', () => { + const T = { [Kind]: 'Kind' } as TSchema + const R = Value.Convert(T, { x: 1 }) + Assert.IsEqual(R, { x: 1 }) + }) + it('Should not convert value 5', () => { + const T = { [Kind]: 'Kind' } as TSchema + const R = Value.Convert(T, [0, 1]) + Assert.IsEqual(R, [0, 1]) + }) +}) diff --git a/test/runtime/value/convert/literal.ts b/test/runtime/value/convert/literal.ts new file mode 100644 index 000000000..c85ac2441 --- /dev/null +++ b/test/runtime/value/convert/literal.ts @@ -0,0 +1,85 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/convert/LiteralString', () => { + it('Should convert from number 1', () => { + const T = Type.Literal('1') + const R = Value.Convert(T, 1) + Assert.IsEqual(R, '1') + }) + it('Should convert from number 2', () => { + const T = Type.Literal('1') + const R = Value.Convert(T, 2) + Assert.IsEqual(R, 2) + }) + it('Should convert from boolean', () => { + const T = Type.Literal('true') + const R = Value.Convert(T, true) + Assert.IsEqual(R, 'true') + }) +}) +describe('value/convert/LiteralNumber', () => { + it('Should convert from number 1', () => { + const T = Type.Literal(3.14) + const R = Value.Convert(T, '3.14') + Assert.IsEqual(R, 3.14) + }) + it('Should convert from number 2', () => { + const T = Type.Literal(3.14) + const R = Value.Convert(T, '3.15') + Assert.IsEqual(R, '3.15') + }) + it('Should convert from boolean 1', () => { + const T = Type.Literal(1) + const R = Value.Convert(T, true) + Assert.IsEqual(R, 1) + }) + it('Should convert from boolean 2', () => { + const T = Type.Literal(0) + const R = Value.Convert(T, false) + Assert.IsEqual(R, 0) + }) + it('Should convert from boolean 3', () => { + const T = Type.Literal(2) + const R = Value.Convert(T, true) + Assert.IsEqual(R, true) + }) +}) +describe('value/convert/LiteralBoolean', () => { + it('Should convert from number 1', () => { + const T = Type.Literal(true) + const R = Value.Convert(T, 3.14) + Assert.IsEqual(R, 3.14) + }) + it('Should convert from number 2', () => { + const T = Type.Literal(true) + const R = Value.Convert(T, 1) + Assert.IsEqual(R, true) + }) + it('Should convert from string 1', () => { + const T = Type.Literal(true) + const R = Value.Convert(T, 'true') + Assert.IsEqual(R, true) + }) + it('Should convert from string 2', () => { + const T = Type.Literal(false) + const R = Value.Convert(T, 'false') + Assert.IsEqual(R, false) + }) + it('Should convert from string 3', () => { + const T = Type.Literal(true) + const R = Value.Convert(T, '1') + Assert.IsEqual(R, true) + }) + it('Should convert from string 4', () => { + const T = Type.Literal(false) + const R = Value.Convert(T, '0') + Assert.IsEqual(R, false) + }) + it('Should convert from string 5', () => { + const T = Type.Literal(false) + const R = Value.Convert(T, '2') + Assert.IsEqual(R, '2') + }) +}) diff --git a/test/runtime/value/convert/never.ts b/test/runtime/value/convert/never.ts new file mode 100644 index 000000000..f0cea44bd --- /dev/null +++ b/test/runtime/value/convert/never.ts @@ -0,0 +1,21 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/convert/Never', () => { + it('Should not convert 1', () => { + const T = Type.Never() + const R = Value.Convert(T, true) + Assert.IsEqual(R, true) + }) + it('Should not convert 2', () => { + const T = Type.Never() + const R = Value.Convert(T, 42) + Assert.IsEqual(R, 42) + }) + it('Should not convert 3', () => { + const T = Type.Never() + const R = Value.Convert(T, 'true') + Assert.IsEqual(R, 'true') + }) +}) diff --git a/test/runtime/value/convert/null.ts b/test/runtime/value/convert/null.ts new file mode 100644 index 000000000..f490e8bec --- /dev/null +++ b/test/runtime/value/convert/null.ts @@ -0,0 +1,19 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/convert/Null', () => { + const T = Type.Null() + it('Should convert from string 1', () => { + const R = Value.Convert(T, 'null') + Assert.IsEqual(R, null) + }) + it('Should convert from string 2', () => { + const R = Value.Convert(T, 'NULL') + Assert.IsEqual(R, null) + }) + it('Should convert from string 3', () => { + const R = Value.Convert(T, 'nil') + Assert.IsEqual(R, 'nil') + }) +}) diff --git a/test/runtime/value/convert/number.ts b/test/runtime/value/convert/number.ts new file mode 100644 index 000000000..8c151db12 --- /dev/null +++ b/test/runtime/value/convert/number.ts @@ -0,0 +1,75 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/convert/Number', () => { + const T = Type.Number() + it('Should convert from string 1', () => { + const R = Value.Convert(T, '3.14') + Assert.IsEqual(R, 3.14) + }) + it('Should convert from string 2', () => { + const R = Value.Convert(T, '42') + Assert.IsEqual(R, 42) + }) + it('Should convert from boolean 1', () => { + const R = Value.Convert(T, true) + Assert.IsEqual(R, 1) + }) + it('Should convert from boolean 2', () => { + const R = Value.Convert(T, false) + Assert.IsEqual(R, 0) + }) + it('Should convert from number 1', () => { + const R = Value.Convert(T, 3.14) + Assert.IsEqual(R, 3.14) + }) + // ---------------------------------------------------------- + // Casts + // ---------------------------------------------------------- + it('Should convert string #1', () => { + const value = 'hello' + const result = Value.Convert(Type.Number(), value) + Assert.IsEqual(result, 'hello') + }) + it('Should convert string #2', () => { + const value = '3.14' + const result = Value.Convert(Type.Number(), value) + Assert.IsEqual(result, 3.14) + }) + it('Should convert string #3', () => { + const value = '-0' + const result = Value.Convert(Type.Number(), value) + Assert.IsEqual(result, -0) + }) + it('Should convert string #4', () => { + const value = '-100' + const result = Value.Convert(Type.Number(), value) + Assert.IsEqual(result, -100) + }) + it('Should convert number', () => { + const value = 42 + const result = Value.Convert(Type.Number(), value) + Assert.IsEqual(result, 42) + }) + it('Should convert true', () => { + const value = true + const result = Value.Convert(Type.Number(), value) + Assert.IsEqual(result, 1) + }) + it('Should convert false', () => { + const value = false + const result = Value.Convert(Type.Number(), value) + Assert.IsEqual(result, 0) + }) + it('Should convert object', () => { + const value = {} + const result = Value.Convert(Type.String(), value) + Assert.IsEqual(result, {}) + }) + it('Should convert array', () => { + const value = [] as any[] + const result = Value.Convert(Type.String(), value) + Assert.IsEqual(result, []) + }) +}) diff --git a/test/runtime/value/convert/object.ts b/test/runtime/value/convert/object.ts new file mode 100644 index 000000000..69a8564be --- /dev/null +++ b/test/runtime/value/convert/object.ts @@ -0,0 +1,29 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +// prettier-ignore +describe('value/convert/Object', () => { + it('Should convert properties', () => { + const T = Type.Object({ + x: Type.Number(), + y: Type.Boolean(), + z: Type.Boolean() + }) + const R = Value.Convert(T, { x: '42', y: 'true', z: 'hello' }) + Assert.IsEqual(R, { x: 42, y: true, z: 'hello' }) + }) + it('Should convert known properties', () => { + const T = Type.Object({ + x: Type.Number(), + y: Type.Boolean() + }) + const R = Value.Convert(T, { x: '42', y: 'true', z: 'hello' }) + Assert.IsEqual(R, { x: 42, y: true, z: 'hello' }) + }) + it('Should not convert missing properties', () => { + const T = Type.Object({ x: Type.Number() }) + const R = Value.Convert(T, { }) + Assert.IsEqual(R, { }) + }) +}) diff --git a/test/runtime/value/convert/promise.ts b/test/runtime/value/convert/promise.ts new file mode 100644 index 000000000..9d0439277 --- /dev/null +++ b/test/runtime/value/convert/promise.ts @@ -0,0 +1,17 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/convert/Promise', () => { + const T = Type.Promise(Type.Any()) + it('Should passthrough 1', () => { + const V = Promise.resolve(1) + const R = Value.Convert(T, V) + Assert.IsEqual(R, V) + }) + it('Should passthrough 2', () => { + const V = 1 + const R = Value.Convert(T, V) + Assert.IsEqual(R, V) + }) +}) diff --git a/test/runtime/value/convert/record.ts b/test/runtime/value/convert/record.ts new file mode 100644 index 000000000..edd3b5a8a --- /dev/null +++ b/test/runtime/value/convert/record.ts @@ -0,0 +1,44 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/convert/Record', () => { + it('Should convert record value to numeric', () => { + const T = Type.Record(Type.String(), Type.Number()) + const V = Value.Convert(T, { x: '42', y: '24', z: 'hello' }) + Assert.IsEqual(V, { x: 42, y: 24, z: 'hello' }) + }) + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/930 + // ---------------------------------------------------------------- + it('Should convert record union 1', () => { + const T = Type.Union([Type.Null(), Type.Record(Type.Number(), Type.Any())]) + const V = Value.Convert(T, {}) + Assert.IsEqual(V, {}) + }) + it('Should convert record union 2', () => { + const T = Type.Union([Type.Record(Type.Number(), Type.Any()), Type.Null()]) + const V = Value.Convert(T, {}) + Assert.IsEqual(V, {}) + }) + it('Should convert record union 3', () => { + const T = Type.Union([Type.Null(), Type.Record(Type.Number(), Type.Any())]) + const V = Value.Convert(T, null) + Assert.IsEqual(V, null) + }) + it('Should convert record union 4', () => { + const T = Type.Union([Type.Record(Type.Number(), Type.Any()), Type.Null()]) + const V = Value.Convert(T, null) + Assert.IsEqual(V, null) + }) + it('Should convert record union 5', () => { + const T = Type.Union([Type.Null(), Type.Record(Type.Number(), Type.Any())]) + const V = Value.Convert(T, 'NULL') + Assert.IsEqual(V, null) + }) + it('Should convert record union 6', () => { + const T = Type.Union([Type.Record(Type.Number(), Type.Any()), Type.Null()]) + const V = Value.Convert(T, 'NULL') + Assert.IsEqual(V, null) + }) +}) diff --git a/test/runtime/value/convert/recursive.ts b/test/runtime/value/convert/recursive.ts new file mode 100644 index 000000000..5cae25261 --- /dev/null +++ b/test/runtime/value/convert/recursive.ts @@ -0,0 +1,7 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/convert/Recursive', () => { + it('Should convert', () => {}) +}) diff --git a/test/runtime/value/convert/regexp.ts b/test/runtime/value/convert/regexp.ts new file mode 100644 index 000000000..c2282b47b --- /dev/null +++ b/test/runtime/value/convert/regexp.ts @@ -0,0 +1,7 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/convert/RegExp', () => { + it('Should convert', () => {}) +}) diff --git a/test/runtime/value/convert/string.ts b/test/runtime/value/convert/string.ts new file mode 100644 index 000000000..5ff0a8726 --- /dev/null +++ b/test/runtime/value/convert/string.ts @@ -0,0 +1,69 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/convert/String', () => { + const T = Type.String() + it('Should convert from number 1', () => { + const R = Value.Convert(T, 3.14) + Assert.IsEqual(R, '3.14') + }) + it('Should convert from number 2', () => { + const R = Value.Convert(T, 3) + Assert.IsEqual(R, '3') + }) + it('Should convert from boolean 1', () => { + const R = Value.Convert(T, true) + Assert.IsEqual(R, 'true') + }) + it('Should convert from boolean 2', () => { + const R = Value.Convert(T, false) + Assert.IsEqual(R, 'false') + }) + it('Should convert from bigint', () => { + const R = Value.Convert(T, BigInt(12345)) + Assert.IsEqual(R, '12345') + }) + it('Should convert from symbol', () => { + const R = Value.Convert(T, Symbol(12345)) + Assert.IsEqual(R, '12345') + }) + // ---------------------------------------------------------- + // Casts + // ---------------------------------------------------------- + it('Should convert string', () => { + const value = 'hello' + const result = Value.Convert(Type.String(), value) + Assert.IsEqual(result, 'hello') + }) + it('Should convert number #1', () => { + const value = 42 + const result = Value.Convert(Type.String(), value) + Assert.IsEqual(result, '42') + }) + it('Should convert number #2', () => { + const value = 42n + const result = Value.Convert(Type.String(), value) + Assert.IsEqual(result, '42') + }) + it('Should convert true', () => { + const value = true + const result = Value.Convert(Type.String(), value) + Assert.IsEqual(result, 'true') + }) + it('Should convert false', () => { + const value = false + const result = Value.Convert(Type.String(), value) + Assert.IsEqual(result, 'false') + }) + it('Should convert object', () => { + const value = {} + const result = Value.Convert(Type.String(), value) + Assert.IsEqual(result, {}) + }) + it('Should convert array', () => { + const value = [] as any[] + const result = Value.Convert(Type.String(), value) + Assert.IsEqual(result, []) + }) +}) diff --git a/test/runtime/value/convert/symbol.ts b/test/runtime/value/convert/symbol.ts new file mode 100644 index 000000000..6ef559da2 --- /dev/null +++ b/test/runtime/value/convert/symbol.ts @@ -0,0 +1,15 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/convert/Symbol', () => { + const T = Type.Symbol() + it('Should convert from number 1', () => { + const R = Value.Convert(T, 3.14) as Symbol + Assert.IsEqual(R.description, '3.14') + }) + it('Should convert from number 2', () => { + const R = Value.Convert(T, 3) as Symbol + Assert.IsEqual(R.description, '3') + }) +}) diff --git a/test/runtime/value/convert/tuple.ts b/test/runtime/value/convert/tuple.ts new file mode 100644 index 000000000..663caf0f8 --- /dev/null +++ b/test/runtime/value/convert/tuple.ts @@ -0,0 +1,21 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/convert/Tuple', () => { + it('Should convert from Array 1', () => { + const T = Type.Tuple([Type.Number(), Type.Number()]) + const R = Value.Convert(T, ['1', 'true']) + Assert.IsEqual(R, [1, 1]) + }) + it('Should convert from Array 2', () => { + const T = Type.Tuple([Type.Number(), Type.Number()]) + const R = Value.Convert(T, ['1']) + Assert.IsEqual(R, [1]) + }) + it('Should convert from Array 3', () => { + const T = Type.Tuple([Type.Number(), Type.Number()]) + const R = Value.Convert(T, ['1', '2', '3']) + Assert.IsEqual(R, [1, 2, '3']) + }) +}) diff --git a/test/runtime/value/convert/uint8array.ts b/test/runtime/value/convert/uint8array.ts new file mode 100644 index 000000000..bd6ac1fb0 --- /dev/null +++ b/test/runtime/value/convert/uint8array.ts @@ -0,0 +1,9 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/convert/Uint8Array', () => { + it('Should convert from Array', () => { + const T = Type.Uint8Array() + }) +}) diff --git a/test/runtime/value/convert/undefined.ts b/test/runtime/value/convert/undefined.ts new file mode 100644 index 000000000..3feab7e40 --- /dev/null +++ b/test/runtime/value/convert/undefined.ts @@ -0,0 +1,15 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/convert/Undefined', () => { + const T = Type.Undefined() + it('Should convert from string 1', () => { + const R = Value.Convert(T, 'undefined') + Assert.IsEqual(R, undefined) + }) + it('Should convert from string 2', () => { + const R = Value.Convert(T, 'hello') + Assert.IsEqual(R, 'hello') + }) +}) diff --git a/test/runtime/value/convert/union.ts b/test/runtime/value/convert/union.ts new file mode 100644 index 000000000..03483da86 --- /dev/null +++ b/test/runtime/value/convert/union.ts @@ -0,0 +1,102 @@ +import { Convert, Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/convert/Union', () => { + it('Should convert union variant', () => { + const T = Type.Object({ + x: Type.Union([Type.Number(), Type.Null()]), + }) + const V1 = Value.Convert(T, { x: '42' }) + const V2 = Value.Convert(T, { x: 'null' }) + const V3 = Value.Convert(T, { x: 'hello' }) + Assert.IsEqual(V1, { x: 42 }) + Assert.IsEqual(V2, { x: null }) + Assert.IsEqual(V3, { x: 'hello' }) + }) + it('Should convert first variant in ambiguous conversion', () => { + const T = Type.Object({ + x: Type.Union([Type.Boolean(), Type.Number()]), + }) + const V1 = Value.Convert(T, { x: '1' }) + Assert.IsEqual(V1, { x: true }) + }) + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/787 + // ---------------------------------------------------------------- + // prettier-ignore + it('Should convert Intersect Union', () => { + const T = Type.Intersect([ + Type.Union([ + Type.Object({ a: Type.Number() }), + Type.Object({ b: Type.Number() }), + ]), + Type.Object({ c: Type.Number() }), + ]) + const A = Convert(T, { a: '1', c: '2' }) + const B = Convert(T, { b: '1', c: '2' }) + const C = Convert(T, { a: '1', b: '2', c: '3' }) + Assert.IsEqual(A, { a: 1, c: 2 }) + Assert.IsEqual(B, { b: 1, c: 2 }) + Assert.IsEqual(C, { a: 1, b: '2', c: 3 }) // note: matching on first + }) + it('Should preserve number type in string-first union when value already matches', () => { + const T = Type.Union([Type.String(), Type.Number()]) + const numValue = 42 + const strValue = 'hello' + + const A = Value.Convert(T, numValue) + const B = Value.Convert(T, strValue) + + Assert.IsEqual(typeof A, 'number') + Assert.IsEqual(typeof B, 'string') + Assert.IsEqual(A, 42) + Assert.IsEqual(B, 'hello') + }) + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/1281 + // Union conversion should preserve original type if valid + // ---------------------------------------------------------------- + it('Should preserve original type when value already matches union variant', () => { + const T1 = Type.Object({ + data: Type.Array(Type.Record(Type.String(), Type.Union([Type.String(), Type.Number(), Type.Null()]))), + }) + const T2 = Type.Object({ + data: Type.Array(Type.Record(Type.String(), Type.Union([Type.Number(), Type.String(), Type.Null()]))), + }) + const testData = { + data: [{ key1: 'hello', key2: 42, key3: null }], + } + + const A = Value.Convert(T1, testData) + const B = Value.Convert(T2, testData) + + // Both should preserve the original number type + Assert.IsEqual(typeof (A as any).data[0].key2, 'number') + Assert.IsEqual(typeof (B as any).data[0].key2, 'number') + Assert.IsEqual((A as any).data[0].key2, 42) + Assert.IsEqual((B as any).data[0].key2, 42) + }) + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/1295 + // ---------------------------------------------------------------- + it('Should guard against Array conversion in Object', () => { + const T = Type.Union([ + Type.Object({ + type: Type.Literal('A'), + values: Type.Union([Type.String(), Type.Number()]), + }), + Type.Object({ + type: Type.Literal('B'), + values: Type.String(), + }), + ]) + const converted = Value.Convert(T, [{ type: 'A', values: 1 }]) + Assert.IsEqual(converted, [{ type: 'A', values: 1 }]) + }) + it('Should guard against Array conversion in Record', () => { + const T = Type.Union([Type.Record(Type.String({ pattern: '^values$' }), Type.Union([Type.String(), Type.Number()])), Type.Record(Type.String({ pattern: '^type$' }), Type.Union([Type.String(), Type.Number()]))]) + const converted = Value.Convert(T, [{ type: 'A', values: 1 }]) + Assert.IsEqual(converted, [{ type: 'A', values: 1 }]) + }) +}) diff --git a/test/runtime/value/convert/unknown.ts b/test/runtime/value/convert/unknown.ts new file mode 100644 index 000000000..d3ca48479 --- /dev/null +++ b/test/runtime/value/convert/unknown.ts @@ -0,0 +1,42 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/convert/Unknown', () => { + const T = Type.Unknown() + it('Should convert null', () => { + const V = null + const R = Value.Convert(T, V) + Assert.IsEqual(R, V) + }) + it('Should convert undefined', () => { + const V = undefined + const R = Value.Convert(T, V) + Assert.IsEqual(R, V) + }) + it('Should convert string', () => { + const V = 'hello' + const R = Value.Convert(T, V) + Assert.IsEqual(R, V) + }) + it('Should convert number', () => { + const V = 42 + const R = Value.Convert(T, V) + Assert.IsEqual(R, V) + }) + it('Should convert boolean', () => { + const V = true + const R = Value.Convert(T, V) + Assert.IsEqual(R, V) + }) + it('Should convert object', () => { + const V = { x: 1 } + const R = Value.Convert(T, V) + Assert.IsEqual(R, V) + }) + it('Should convert array', () => { + const V = [1, 2, 3] + const R = Value.Convert(T, V) + Assert.IsEqual(R, V) + }) +}) diff --git a/test/runtime/value/convert/void.ts b/test/runtime/value/convert/void.ts new file mode 100644 index 000000000..a977b79d5 --- /dev/null +++ b/test/runtime/value/convert/void.ts @@ -0,0 +1,7 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/convert/Void', () => { + it('Should convert', () => {}) +}) diff --git a/test/runtime/value/create/_deferred.ts b/test/runtime/value/create/_deferred.ts new file mode 100644 index 000000000..6b2a86cac --- /dev/null +++ b/test/runtime/value/create/_deferred.ts @@ -0,0 +1,11 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/create/Deferred', () => { + it('Should use deferred value', () => { + const T = Type.Any({ default: () => 1 }) + const R = Value.Create(T) + Assert.IsEqual(R, 1) + }) +}) diff --git a/test/runtime/value/create/any.ts b/test/runtime/value/create/any.ts new file mode 100644 index 000000000..44809c7d1 --- /dev/null +++ b/test/runtime/value/create/any.ts @@ -0,0 +1,14 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/create/Any', () => { + it('Should create value', () => { + const T = Type.Any() + Assert.IsEqual(Value.Create(T), {}) + }) + it('Should create default', () => { + const T = Type.Any({ default: 1 }) + Assert.IsEqual(Value.Create(T), 1) + }) +}) diff --git a/test/runtime/value/create/argument.ts b/test/runtime/value/create/argument.ts new file mode 100644 index 000000000..55bd0de07 --- /dev/null +++ b/test/runtime/value/create/argument.ts @@ -0,0 +1,10 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/create/Argument', () => { + it('Should create value', () => { + const T = Type.Argument(0) + Assert.IsEqual(Value.Create(T), {}) + }) +}) diff --git a/test/runtime/value/create/array.ts b/test/runtime/value/create/array.ts new file mode 100644 index 000000000..ae042f8ce --- /dev/null +++ b/test/runtime/value/create/array.ts @@ -0,0 +1,18 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/create/Array', () => { + it('Should create value', () => { + const T = Type.Array(Type.String()) + Assert.IsEqual(Value.Create(T), []) + }) + it('Should create default', () => { + const T = Type.Array(Type.String(), { default: ['1'] }) + Assert.IsEqual(Value.Create(T), ['1']) + }) + it('Should create with minItems', () => { + const T = Type.Array(Type.String(), { minItems: 4 }) + Assert.IsEqual(Value.Create(T), ['', '', '', '']) + }) +}) diff --git a/test/runtime/value/create/async-iterator.ts b/test/runtime/value/create/async-iterator.ts new file mode 100644 index 000000000..da2516908 --- /dev/null +++ b/test/runtime/value/create/async-iterator.ts @@ -0,0 +1,16 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/create/AsyncIterator', () => { + it('Should create value', () => { + const T = Type.AsyncIterator(Type.Any()) + const R = Value.Create(T) + Assert.IsTrue(Symbol.asyncIterator in R) + }) + it('Should create default', () => { + const T = Type.AsyncIterator(Type.Any(), { default: 1 }) + const R = Value.Create(T) + Assert.IsEqual(R, 1) + }) +}) diff --git a/test/runtime/value/create/bigint.ts b/test/runtime/value/create/bigint.ts new file mode 100644 index 000000000..9f53207c5 --- /dev/null +++ b/test/runtime/value/create/bigint.ts @@ -0,0 +1,14 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/create/BigInt', () => { + it('Should create value', () => { + const T = Type.BigInt() + Assert.IsEqual(Value.Create(T), BigInt(0)) + }) + it('Should create default', () => { + const T = Type.BigInt({ default: true }) + Assert.IsEqual(Value.Create(T), true) + }) +}) diff --git a/test/runtime/value/create/boolean.ts b/test/runtime/value/create/boolean.ts new file mode 100644 index 000000000..28c1c4ea2 --- /dev/null +++ b/test/runtime/value/create/boolean.ts @@ -0,0 +1,14 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/create/Boolean', () => { + it('Should create value', () => { + const T = Type.Boolean() + Assert.IsEqual(Value.Create(T), false) + }) + it('Should create default', () => { + const T = Type.Boolean({ default: true }) + Assert.IsEqual(Value.Create(T), true) + }) +}) diff --git a/test/runtime/value/create/composite.ts b/test/runtime/value/create/composite.ts new file mode 100644 index 000000000..a19f506bc --- /dev/null +++ b/test/runtime/value/create/composite.ts @@ -0,0 +1,66 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/create/Composite', () => { + it('Should create value', () => { + const A = Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }) + const B = Type.Object({ + a: Type.Number(), + b: Type.Number(), + c: Type.Number(), + }) + const T = Type.Composite([A, B]) + Assert.IsEqual(Value.Create(T), { + x: 0, + y: 0, + z: 0, + a: 0, + b: 0, + c: 0, + }) + }) + it('Should create default', () => { + const A = Type.Object({ + x: Type.Number({ default: 1 }), + y: Type.Number({ default: 2 }), + z: Type.Number({ default: 3 }), + }) + const B = Type.Object({ + a: Type.Number({ default: 4 }), + b: Type.Number({ default: 5 }), + c: Type.Number({ default: 6 }), + }) + const T = Type.Composite([A, B]) + Assert.IsEqual(Value.Create(T), { + x: 1, + y: 2, + z: 3, + a: 4, + b: 5, + c: 6, + }) + }) + it('Should create default and omit optionals', () => { + const A = Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }) + const B = Type.Object({ + a: Type.Optional(Type.Number()), + b: Type.Optional(Type.Number()), + c: Type.Optional(Type.Number()), + }) + const T = Type.Composite([A, B]) + Assert.IsEqual(Value.Create(T), { + x: 0, + y: 0, + z: 0, + }) + }) +}) diff --git a/test/runtime/value/create/constructor.ts b/test/runtime/value/create/constructor.ts new file mode 100644 index 000000000..d42dc85c0 --- /dev/null +++ b/test/runtime/value/create/constructor.ts @@ -0,0 +1,38 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/create/Constructor', () => { + it('Should create value', () => { + const T = Type.Constructor( + [], + Type.Object({ + test: Type.Function([], Type.Number({ default: 123 })), + }), + ) + const C = Value.Create(T) + const I = new C() + const R = I.test() + Assert.IsEqual(R, 123) + }) + it('Should create default', () => { + const T = Type.Constructor( + [], + Type.Object({ + test: Type.Function([], Type.Number({ default: 123 })), + }), + { + default: () => + class { + test() { + return 321 + } + }, + }, + ) + const C = Value.Create(T) + const I = new C() + const R = I.test() + Assert.IsEqual(R, 321) + }) +}) diff --git a/test/runtime/value/create/date.ts b/test/runtime/value/create/date.ts new file mode 100644 index 000000000..37c12b7ee --- /dev/null +++ b/test/runtime/value/create/date.ts @@ -0,0 +1,30 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/create/Date', () => { + it('Should create value', () => { + const T = Type.Date() + const A = Value.Create(T) + const B = new Date() + Assert.InRange(A.getTime(), B.getTime(), 1000) + }) + it('Should create default', () => { + const T = Type.Date({ default: new Date(1001) }) + const A = Value.Create(T) + const B = new Date(1001) + Assert.IsEqual(A, B) + }) + it('Should create value nested', () => { + const T = Type.Object({ value: Type.Date() }) + const A = Value.Create(T) + const B = { value: new Date() } + Assert.InRange(A.value.getTime(), B.value.getTime(), 1000) + }) + it('Should create default nested', () => { + const T = Type.Object({ value: Type.Date({ default: new Date(1001) }) }) + const A = Value.Create(T) + const B = { value: new Date(1001) } + Assert.IsEqual(A, B) + }) +}) diff --git a/test/runtime/value/create/enum.ts b/test/runtime/value/create/enum.ts new file mode 100644 index 000000000..81acdc6f0 --- /dev/null +++ b/test/runtime/value/create/enum.ts @@ -0,0 +1,22 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/create/Boolean', () => { + it('Should create value', () => { + enum Foo { + A, + B, + } + const T = Type.Enum(Foo) + Assert.IsEqual(Value.Create(T), Foo.A) + }) + it('Should create default', () => { + enum Foo { + A, + B, + } + const T = Type.Enum(Foo, { default: Foo.B }) + Assert.IsEqual(Value.Create(T), Foo.B) + }) +}) diff --git a/test/runtime/value/create/function.ts b/test/runtime/value/create/function.ts new file mode 100644 index 000000000..7495bd076 --- /dev/null +++ b/test/runtime/value/create/function.ts @@ -0,0 +1,18 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/create/Function', () => { + it('Should create value', () => { + const T = Type.Function([], Type.Number({ default: 123 })) + const F = Value.Create(T) + const R = F() + Assert.IsEqual(R, 123) + }) + it('Should create default', () => { + const T = Type.Function([], Type.Number({ default: 123 }), { default: () => () => 321 }) + const F = Value.Create(T) + const R = F() + Assert.IsEqual(R, 321) + }) +}) diff --git a/test/runtime/value/create/import.ts b/test/runtime/value/create/import.ts new file mode 100644 index 000000000..b07f96dfa --- /dev/null +++ b/test/runtime/value/create/import.ts @@ -0,0 +1,98 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/create/Import', () => { + it('Should create value', () => { + const T = Type.Module({ + A: Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }), + }).Import('A') + Assert.IsEqual(Value.Create(T), { + x: 0, + y: 0, + z: 0, + }) + }) + it('Should create value with optional properties', () => { + const T = Type.Module({ + A: Type.Object({ + x: Type.Optional(Type.Number()), + y: Type.Optional(Type.Number()), + z: Type.Optional(Type.Number()), + }), + }).Import('A') + Assert.IsEqual(Value.Create(T), {}) + }) + it('Should create default with default properties', () => { + const T = Type.Module({ + A: Type.Object({ + x: Type.Number({ default: 1 }), + y: Type.Number({ default: 2 }), + z: Type.Number({ default: 3 }), + }), + }).Import('A') + Assert.IsEqual(Value.Create(T), { + x: 1, + y: 2, + z: 3, + }) + }) + it('Should create nested object', () => { + const T = Type.Module({ + A: Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + w: Type.Object({ + x: Type.Number({ default: 7 }), + y: Type.Number(), + z: Type.Number(), + }), + }), + }).Import('A') + Assert.IsEqual(Value.Create(T), { + x: 0, + y: 0, + z: 0, + w: { x: 7, y: 0, z: 0 }, + }) + }) + it('Should create with default', () => { + const T = Type.Module({ + A: Type.Object( + { + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }, + { default: { x: 1, y: 2, z: 3 } }, + ), + }).Import('A') + Assert.IsEqual(Value.Create(T), { + x: 1, + y: 2, + z: 3, + }) + }) + // ---------------------------------------------------------------- + // Mutation + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/726 + it('Should clone defaults on assignment - no mutation', () => { + const T = Type.Module({ + A: Type.Object( + { + x: Type.Number(), + }, + { default: { x: 1 } }, + ), + }).Import('A') + const V = Value.Create(T) + V.x = 123 + Assert.IsEqual(T.$defs.A.default, { x: 1 }) + }) +}) diff --git a/test/runtime/value/create/index.ts b/test/runtime/value/create/index.ts new file mode 100644 index 000000000..78c340b5e --- /dev/null +++ b/test/runtime/value/create/index.ts @@ -0,0 +1,37 @@ +import './_deferred' +import './any' +import './argument' +import './array' +import './async-iterator' +import './bigint' +import './boolean' +import './composite' +import './constructor' +import './date' +import './enum' +import './function' +import './import' +import './integer' +import './intersect' +import './iterator' +import './keyof' +import './kind' +import './literal' +import './never' +import './not' +import './null' +import './number' +import './object' +import './recursive' +import './ref' +import './record' +import './regexp' +import './string' +import './symbol' +import './template-literal' +import './tuple' +import './uint8array' +import './undefined' +import './union' +import './unknown' +import './void' diff --git a/test/runtime/value/create/integer.ts b/test/runtime/value/create/integer.ts new file mode 100644 index 000000000..f6dc35b39 --- /dev/null +++ b/test/runtime/value/create/integer.ts @@ -0,0 +1,14 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/create/Integer', () => { + it('Should create value', () => { + const T = Type.Integer() + Assert.IsEqual(Value.Create(T), 0) + }) + it('Should create default', () => { + const T = Type.Integer({ default: 7 }) + Assert.IsEqual(Value.Create(T), 7) + }) +}) diff --git a/test/runtime/value/create/intersect.ts b/test/runtime/value/create/intersect.ts new file mode 100644 index 000000000..771e07e2b --- /dev/null +++ b/test/runtime/value/create/intersect.ts @@ -0,0 +1,61 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/create/Intersect', () => { + it('Should create value', () => { + const T = Type.Intersect([Type.Object({ x: Type.Number() }), Type.Object({ y: Type.Number() })]) + const R = Value.Create(T) + Assert.IsEqual(R, { x: 0, y: 0 }) + }) + it('Should create value with default', () => { + const T = Type.Intersect([Type.Object({ x: Type.Number({ default: 100 }) }), Type.Object({ y: Type.Number({ default: 200 }) })]) + const R = Value.Create(T) + Assert.IsEqual(R, { x: 100, y: 200 }) + }) + it('Should create for overlapping intersection', () => { + const T = Type.Intersect([Type.Object({ x: Type.Number() }), Type.Object({ x: Type.Number(), y: Type.Number() })]) + const R = Value.Create(T) + Assert.IsEqual(R, { x: 0, y: 0 }) + }) + it('Should create with last intersected overlapping default', () => { + const T = Type.Intersect([Type.Object({ x: Type.Number({ default: 1 }) }), Type.Object({ x: Type.Number({ default: 2 }), y: Type.Number() })]) + const R = Value.Create(T) + Assert.IsEqual(R, { x: 2, y: 0 }) + }) + it('Should throw for non-constructable intersection 1', () => { + const T = Type.Intersect([Type.Object({ x: Type.Number() }), Type.Object({ x: Type.String() })]) + Assert.Throws(() => Value.Create(T)) + }) + it('Should throw for non-constructable intersection 2', () => { + const T = Type.Intersect([Type.String(), Type.Number()]) + Assert.Throws(() => Value.Create(T)) + }) + it('Should not throw for non-constructable intersection with default', () => { + const T = Type.Intersect([Type.String(), Type.Number()], { default: 'hello' }) + const R = Value.Create(T) + Assert.IsEqual(R, 'hello') + }) + it('Should create from nested intersection', () => { + const T = Type.Intersect([ + Type.Object({ + x: Type.Number({ default: 1 }), + }), + Type.Intersect([ + Type.Object({ + y: Type.Number({ default: 2 }), + }), + Type.Object({ + z: Type.Number({ default: 3 }), + }), + ]), + ]) + const R = Value.Create(T) + Assert.IsEqual(R, { x: 1, y: 2, z: 3 }) + }) + it('Should create non varying primitive', () => { + const T = Type.Intersect([Type.Number(), Type.Number(), Type.Number()]) + const R = Value.Create(T) + Assert.IsEqual(R, 0) + }) +}) diff --git a/test/runtime/value/create/iterator.ts b/test/runtime/value/create/iterator.ts new file mode 100644 index 000000000..02ea1c991 --- /dev/null +++ b/test/runtime/value/create/iterator.ts @@ -0,0 +1,16 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/create/Iterator', () => { + it('Should create value', () => { + const T = Type.Iterator(Type.Any()) + const R = Value.Create(T) + Assert.IsTrue(Symbol.iterator in R) + }) + it('Should create default', () => { + const T = Type.Iterator(Type.Any(), { default: 1 }) + const R = Value.Create(T) + Assert.IsEqual(R, 1) + }) +}) diff --git a/test/runtime/value/create/keyof.ts b/test/runtime/value/create/keyof.ts new file mode 100644 index 000000000..0f4c33e1a --- /dev/null +++ b/test/runtime/value/create/keyof.ts @@ -0,0 +1,25 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/create/KeyOf', () => { + it('Should create value', () => { + const T = Type.KeyOf( + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + ) + Assert.IsEqual(Value.Create(T), 'x') + }) + it('Should create default', () => { + const T = Type.KeyOf( + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + { default: 'y' }, + ) + Assert.IsEqual(Value.Create(T), 'y') + }) +}) diff --git a/test/runtime/value/create/kind.ts b/test/runtime/value/create/kind.ts new file mode 100644 index 000000000..3bb42d242 --- /dev/null +++ b/test/runtime/value/create/kind.ts @@ -0,0 +1,23 @@ +import { Value } from '@sinclair/typebox/value' +import { Type, Kind, TypeRegistry } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/create/Kind', () => { + // --------------------------------------------------------- + // Fixtures + // --------------------------------------------------------- + beforeEach(() => TypeRegistry.Set('Kind', () => true)) + afterEach(() => TypeRegistry.Delete('Kind')) + // --------------------------------------------------------- + // Tests + // --------------------------------------------------------- + it('Should create custom value with default', () => { + const T = Type.Unsafe({ [Kind]: 'Kind', default: 'hello' }) + Assert.IsEqual(Value.Create(T), 'hello') + }) + it('Should throw when no default value is specified', () => { + TypeRegistry.Set('Kind', () => true) + const T = Type.Unsafe({ [Kind]: 'Kind' }) + Assert.Throws(() => Value.Create(T)) + }) +}) diff --git a/test/runtime/value/create/literal.ts b/test/runtime/value/create/literal.ts new file mode 100644 index 000000000..3004dcd97 --- /dev/null +++ b/test/runtime/value/create/literal.ts @@ -0,0 +1,22 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/create/Literal', () => { + it('Should create literal string', () => { + const T = Type.Literal('hello') + Assert.IsEqual(Value.Create(T), 'hello') + }) + it('Should create literal number', () => { + const T = Type.Literal(1) + Assert.IsEqual(Value.Create(T), 1) + }) + it('Should create literal boolean', () => { + const T = Type.Literal(true) + Assert.IsEqual(Value.Create(T), true) + }) + it('Should create literal from default value', () => { + const T = Type.Literal(true, { default: 'hello' }) + Assert.IsEqual(Value.Create(T), 'hello') + }) +}) diff --git a/test/runtime/value/create/never.ts b/test/runtime/value/create/never.ts new file mode 100644 index 000000000..55da1ea13 --- /dev/null +++ b/test/runtime/value/create/never.ts @@ -0,0 +1,10 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/create/Never', () => { + it('Should create value', () => { + const T = Type.Never() + Assert.Throws(() => Value.Create(T)) + }) +}) diff --git a/test/runtime/value/create/not.ts b/test/runtime/value/create/not.ts new file mode 100644 index 000000000..70f1ec2cb --- /dev/null +++ b/test/runtime/value/create/not.ts @@ -0,0 +1,20 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/create/Not', () => { + it('Should throw without default value', () => { + const T = Type.Not(Type.String()) + Assert.Throws(() => Value.Create(T)) + }) + it('Should create value with default inner', () => { + const T = Type.Not(Type.String(), { default: 100 }) + const R = Value.Create(T) + Assert.IsEqual(R, 100) + }) + it('Should create value with default outer', () => { + const T = Type.Not(Type.String(), { default: 100 }) + const R = Value.Create(T) + Assert.IsEqual(R, 100) + }) +}) diff --git a/test/runtime/value/create/null.ts b/test/runtime/value/create/null.ts new file mode 100644 index 000000000..1c13270a7 --- /dev/null +++ b/test/runtime/value/create/null.ts @@ -0,0 +1,14 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/create/Null', () => { + it('Should create value', () => { + const T = Type.Null() + Assert.IsEqual(Value.Create(T), null) + }) + it('Should create null from default value', () => { + const T = Type.Null({ default: 'hello' }) + Assert.IsEqual(Value.Create(T), 'hello') + }) +}) diff --git a/test/runtime/value/create/number.ts b/test/runtime/value/create/number.ts new file mode 100644 index 000000000..756c3aa59 --- /dev/null +++ b/test/runtime/value/create/number.ts @@ -0,0 +1,14 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/create/Number', () => { + it('Should create value', () => { + const T = Type.Number() + Assert.IsEqual(Value.Create(T), 0) + }) + it('Should create default', () => { + const T = Type.Number({ default: 7 }) + Assert.IsEqual(Value.Create(T), 7) + }) +}) diff --git a/test/runtime/value/create/object.ts b/test/runtime/value/create/object.ts new file mode 100644 index 000000000..e915a142b --- /dev/null +++ b/test/runtime/value/create/object.ts @@ -0,0 +1,86 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/create/Object', () => { + it('Should create value', () => { + const T = Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }) + Assert.IsEqual(Value.Create(T), { + x: 0, + y: 0, + z: 0, + }) + }) + it('Should create value with optional properties', () => { + const T = Type.Object({ + x: Type.Optional(Type.Number()), + y: Type.Optional(Type.Number()), + z: Type.Optional(Type.Number()), + }) + Assert.IsEqual(Value.Create(T), {}) + }) + it('Should create default with default properties', () => { + const T = Type.Object({ + x: Type.Number({ default: 1 }), + y: Type.Number({ default: 2 }), + z: Type.Number({ default: 3 }), + }) + Assert.IsEqual(Value.Create(T), { + x: 1, + y: 2, + z: 3, + }) + }) + it('Should create nested object', () => { + const T = Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + w: Type.Object({ + x: Type.Number({ default: 7 }), + y: Type.Number(), + z: Type.Number(), + }), + }) + Assert.IsEqual(Value.Create(T), { + x: 0, + y: 0, + z: 0, + w: { x: 7, y: 0, z: 0 }, + }) + }) + it('Should create with default', () => { + const T = Type.Object( + { + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }, + { default: { x: 1, y: 2, z: 3 } }, + ) + Assert.IsEqual(Value.Create(T), { + x: 1, + y: 2, + z: 3, + }) + }) + // ---------------------------------------------------------------- + // Mutation + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/726 + it('Should clone defaults on assignment - no mutation', () => { + const T = Type.Object( + { + x: Type.Number(), + }, + { default: { x: 1 } }, + ) + const V = Value.Create(T) + V.x = 123 + Assert.IsEqual(T.default, { x: 1 }) + }) +}) diff --git a/test/runtime/value/create/record.ts b/test/runtime/value/create/record.ts new file mode 100644 index 000000000..1a66fcc04 --- /dev/null +++ b/test/runtime/value/create/record.ts @@ -0,0 +1,18 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/create/Record', () => { + it('Should create value', () => { + const T = Type.Record(Type.String(), Type.Object({})) + Assert.IsEqual(Value.Create(T), {}) + }) + it('Should create default', () => { + const T = Type.Record(Type.String(), Type.Object({}), { + default: { + x: {}, + }, + }) + Assert.IsEqual(Value.Create(T), { x: {} }) + }) +}) diff --git a/test/runtime/value/create/recursive.ts b/test/runtime/value/create/recursive.ts new file mode 100644 index 000000000..2fb57e9df --- /dev/null +++ b/test/runtime/value/create/recursive.ts @@ -0,0 +1,53 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/create/Recursive', () => { + it('Should create value', () => { + const T = Type.Recursive((This) => + Type.Object({ + id: Type.String(), + nodes: Type.Array(This), + }), + ) + Assert.IsEqual(Value.Create(T), { + id: '', + nodes: [], + }) + }) + it('Should create default', () => { + const T = Type.Recursive( + (This) => + Type.Object({ + id: Type.String(), + nodes: Type.Array(This), + }), + { default: 7 }, + ) + Assert.IsEqual(Value.Create(T), 7) + }) + it('Should throw on infinite type', () => { + const T = Type.Recursive((This) => + Type.Object({ + x: This, + }), + ) + Assert.Throws(() => Value.Create(T)) + }) + it('Should not throw on recursive type when terminating sub type proceeds self', () => { + const T = Type.Recursive((This) => + Type.Object({ + x: Type.Union([Type.Null(), This]), + }), + ) + Assert.IsEqual(Value.Create(T), { x: null }) + }) + it('Should not throw on recursive type when self is optional', () => { + const T = Type.Recursive((This) => + Type.Object({ + x: Type.Optional(This), + }), + ) + Assert.IsEqual(Value.Create(T), {}) + }) +}) diff --git a/test/runtime/value/create/ref.ts b/test/runtime/value/create/ref.ts new file mode 100644 index 000000000..07142c1a2 --- /dev/null +++ b/test/runtime/value/create/ref.ts @@ -0,0 +1,35 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/create/Ref', () => { + it('Should throw if target is undefined', () => { + const T = Type.Object( + { + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }, + { $id: 'T', default: 'target' }, + ) + const R = Type.Ref('T') + Assert.Throws(() => Value.Create(R)) + }) + it('Should create ref default if ref default is defined', () => { + const T = Type.Object( + { + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }, + { $id: 'T', default: 'target' }, + ) + const R = Type.Ref('T', { default: 'override' }) + Assert.IsEqual(Value.Create(R), 'override') // terminated at R default value + }) + it('Should dereference remote schema via $ref', () => { + const R = Type.Number({ $id: 'R' }) + const T = Type.Object({ x: Type.Ref('R') }) + Assert.IsEqual(Value.Create(T, [R]), { x: 0 }) + }) +}) diff --git a/test/runtime/value/create/regexp.ts b/test/runtime/value/create/regexp.ts new file mode 100644 index 000000000..cc424393c --- /dev/null +++ b/test/runtime/value/create/regexp.ts @@ -0,0 +1,23 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/create/RegEx', () => { + it('Should throw without a default value', () => { + Assert.Throws(() => { + const T = Type.RegExp(/foo/) + Value.Create(T) + }) + }) + it('Should create default', () => { + const T = Type.RegExp(/foo/, { default: 'foo' }) + Assert.IsEqual(Value.Create(T), 'foo') + }) + // ---------------------------------------------------------------- + // Throw + // ---------------------------------------------------------------- + it('Should throw with no default', () => { + const T = Type.RegExp(/foo/) + Assert.Throws(() => Value.Create(T)) + }) +}) diff --git a/test/runtime/value/create/string.ts b/test/runtime/value/create/string.ts new file mode 100644 index 000000000..222f03180 --- /dev/null +++ b/test/runtime/value/create/string.ts @@ -0,0 +1,14 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/create/String', () => { + it('Should create value', () => { + const T = Type.String() + Assert.IsEqual(Value.Create(T), '') + }) + it('Should create default', () => { + const T = Type.String({ default: 'hello' }) + Assert.IsEqual(Value.Create(T), 'hello') + }) +}) diff --git a/test/runtime/value/create/symbol.ts b/test/runtime/value/create/symbol.ts new file mode 100644 index 000000000..90af86fa5 --- /dev/null +++ b/test/runtime/value/create/symbol.ts @@ -0,0 +1,15 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/create/Symbol', () => { + it('Should create value', () => { + const T = Type.Symbol() + const V = Value.Create(T) + Assert.IsEqual(typeof V, 'symbol') + }) + it('Should create default', () => { + const T = Type.Symbol({ default: true }) + Assert.IsEqual(Value.Create(T), true) + }) +}) diff --git a/test/runtime/value/create/template-literal.ts b/test/runtime/value/create/template-literal.ts new file mode 100644 index 000000000..ccd9a84c3 --- /dev/null +++ b/test/runtime/value/create/template-literal.ts @@ -0,0 +1,35 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/create/TemplateLiteral', () => { + it('Should create pattern 1', () => { + const T = Type.TemplateLiteral([Type.Literal('A')]) + const V = Value.Create(T) + Assert.IsEqual(V, 'A') + }) + it('Should create pattern 2', () => { + const T = Type.TemplateLiteral([Type.Literal('A'), Type.Literal('B')]) + const V = Value.Create(T) + Assert.IsEqual(V, 'AB') + }) + it('Should create pattern 3 (first only)', () => { + const T = Type.TemplateLiteral([Type.Literal('A'), Type.Union([Type.Literal('B'), Type.Literal('C')])]) + const V = Value.Create(T) + Assert.IsEqual(V, 'AB') + }) + it('Should create pattern 4 (first only)', () => { + const T = Type.TemplateLiteral([Type.Boolean()]) + const V = Value.Create(T) + Assert.IsEqual(V, 'true') + }) + it('Should throw on infinite pattern', () => { + const T = Type.TemplateLiteral([Type.Number()]) + Assert.Throws(() => Value.Create(T)) + }) + it('Should create on infinite pattern with default', () => { + const T = Type.TemplateLiteral([Type.Number()], { default: 42 }) + const V = Value.Create(T) + Assert.IsEqual(V, 42) + }) +}) diff --git a/test/runtime/value/create/tuple.ts b/test/runtime/value/create/tuple.ts new file mode 100644 index 000000000..085296bba --- /dev/null +++ b/test/runtime/value/create/tuple.ts @@ -0,0 +1,22 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/create/Tuple', () => { + it('Should create value', () => { + const T = Type.Tuple([Type.Number(), Type.String()]) + Assert.IsEqual(Value.Create(T), [0, '']) + }) + it('Should create default', () => { + const T = Type.Tuple([Type.Number(), Type.String()], { default: [7, 'hello'] }) + Assert.IsEqual(Value.Create(T), [7, 'hello']) + }) + it('Should create default elements', () => { + const T = Type.Tuple([Type.Number({ default: 7 }), Type.String({ default: 'hello' })]) + Assert.IsEqual(Value.Create(T), [7, 'hello']) + }) + it('Should create default by overriding elements', () => { + const T = Type.Tuple([Type.Number({ default: 7 }), Type.String({ default: 'hello' })], { default: [32, 'world'] }) + Assert.IsEqual(Value.Create(T), [32, 'world']) + }) +}) diff --git a/test/runtime/value/create/uint8array.ts b/test/runtime/value/create/uint8array.ts new file mode 100644 index 000000000..faf044f43 --- /dev/null +++ b/test/runtime/value/create/uint8array.ts @@ -0,0 +1,34 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/create/Uint8Array', () => { + it('Should create value', () => { + const T = Type.Uint8Array() + const value = Value.Create(T) + Assert.IsInstanceOf(value, Uint8Array) + Assert.IsEqual(value.length, 0) + }) + it('Should create default', () => { + const T = Type.Uint8Array({ default: new Uint8Array([0, 1, 2, 3]) }) + const value = Value.Create(T) + Assert.IsInstanceOf(value, Uint8Array) + Assert.IsEqual(value.length, 4) + Assert.IsEqual([value[0], value[1], value[2], value[3]], [0, 1, 2, 3]) + }) + it('Should create with minByteLength', () => { + const T = Type.Uint8Array({ minByteLength: 4 }) + const value = Value.Create(T) + Assert.IsInstanceOf(value, Uint8Array) + Assert.IsEqual(value.length, 4) + Assert.IsEqual([value[0], value[1], value[2], value[3]], [0, 0, 0, 0]) + }) + it('Should create value nested', () => { + const T = Type.Object({ value: Type.Uint8Array() }) + Assert.IsEqual(Value.Create(T), { value: new Uint8Array() }) + }) + it('Should create default nested', () => { + const T = Type.Object({ value: Type.Date({ default: new Uint8Array([1, 2, 3, 4]) }) }) + Assert.IsEqual(Value.Create(T), { value: new Uint8Array([1, 2, 3, 4]) }) + }) +}) diff --git a/test/runtime/value/create/undefined.ts b/test/runtime/value/create/undefined.ts new file mode 100644 index 000000000..e2f530ee7 --- /dev/null +++ b/test/runtime/value/create/undefined.ts @@ -0,0 +1,14 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/create/Undefined', () => { + it('Should create value', () => { + const T = Type.Undefined() + Assert.IsEqual(Value.Create(T), undefined) + }) + it('Should create value from default value', () => { + const T = Type.Undefined({ default: 'hello' }) + Assert.IsEqual(Value.Create(T), 'hello') + }) +}) diff --git a/test/runtime/value/create/union.ts b/test/runtime/value/create/union.ts new file mode 100644 index 000000000..37da0abaa --- /dev/null +++ b/test/runtime/value/create/union.ts @@ -0,0 +1,49 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/create/Union', () => { + it('Should create union Object', () => { + const A = Type.Object({ + type: Type.Literal('A'), + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }) + const B = Type.Object({ + type: Type.Literal('B'), + x: Type.String(), + y: Type.String(), + z: Type.String(), + }) + const T = Type.Union([A, B]) + Assert.IsEqual(Value.Create(T), { + type: 'A', + x: 0, + y: 0, + z: 0, + }) + }) + it('Should create union Null', () => { + const A = Type.Null() + const B = Type.Object({ + type: Type.Literal('B'), + x: Type.String(), + y: Type.String(), + z: Type.String(), + }) + const T = Type.Union([A, B]) + Assert.IsEqual(Value.Create(T), null) + }) + it('Should create union Array', () => { + const A = Type.Array(Type.String()) + const B = Type.Object({ + type: Type.Literal('B'), + x: Type.String(), + y: Type.String(), + z: Type.String(), + }) + const T = Type.Union([A, B]) + Assert.IsEqual(Value.Create(T), []) + }) +}) diff --git a/test/runtime/value/create/unknown.ts b/test/runtime/value/create/unknown.ts new file mode 100644 index 000000000..a67b3ec8f --- /dev/null +++ b/test/runtime/value/create/unknown.ts @@ -0,0 +1,14 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/create/Unknown', () => { + it('Should create value', () => { + const T = Type.Unknown() + Assert.IsEqual(Value.Create(T), {}) + }) + it('Should create default', () => { + const T = Type.Unknown({ default: 1 }) + Assert.IsEqual(Value.Create(T), 1) + }) +}) diff --git a/test/runtime/value/create/void.ts b/test/runtime/value/create/void.ts new file mode 100644 index 000000000..1bef38828 --- /dev/null +++ b/test/runtime/value/create/void.ts @@ -0,0 +1,14 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/create/Void', () => { + it('Should create value', () => { + const T = Type.Void() + Assert.IsEqual(Value.Create(T), undefined) + }) + it('Should create value from default value', () => { + const T = Type.Void({ default: 'hello' }) + Assert.IsEqual(Value.Create(T), 'hello') + }) +}) diff --git a/test/runtime/value/default/_deferred.ts b/test/runtime/value/default/_deferred.ts new file mode 100644 index 000000000..1fc77c567 --- /dev/null +++ b/test/runtime/value/default/_deferred.ts @@ -0,0 +1,11 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/default/Deferred', () => { + it('Should use deferred value', () => { + const T = Type.Any({ default: () => 1 }) + const R = Value.Default(T, 1) + Assert.IsEqual(R, 1) + }) +}) diff --git a/test/runtime/value/default/any.ts b/test/runtime/value/default/any.ts new file mode 100644 index 000000000..15b12e979 --- /dev/null +++ b/test/runtime/value/default/any.ts @@ -0,0 +1,16 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/default/Any', () => { + it('Should use default', () => { + const T = Type.Any({ default: 1 }) + const R = Value.Default(T, undefined) + Assert.IsEqual(R, 1) + }) + it('Should use value', () => { + const T = Type.Any({ default: 1 }) + const R = Value.Default(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/default/array.ts b/test/runtime/value/default/array.ts new file mode 100644 index 000000000..602f6562c --- /dev/null +++ b/test/runtime/value/default/array.ts @@ -0,0 +1,50 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/default/Array', () => { + it('Should use default', () => { + const T = Type.Array(Type.Number(), { default: 1 }) + const R = Value.Default(T, undefined) + Assert.IsEqual(R, 1) + }) + it('Should use value', () => { + const T = Type.Array(Type.Number(), { default: 1 }) + const R = Value.Default(T, null) + Assert.IsEqual(R, null) + }) + // ---------------------------------------------------------------- + // Elements + // ---------------------------------------------------------------- + it('Should use default on elements', () => { + const T = Type.Array(Type.Number({ default: 2 })) + const R = Value.Default(T, [1, undefined, 3]) + Assert.IsEqual(R, [1, 2, 3]) + }) + // ---------------------------------------------------------------- + // Elements + // ---------------------------------------------------------------- + it('Should should retain array and only initialize undefined elements', () => { + const T = Type.Array(Type.Literal('hello', { default: 'hello' })) + const R = Value.Default(T, [1, undefined, 3]) + Assert.IsEqual(R, [1, 'hello', 3]) + }) + // ---------------------------------------------------------------- + // https://github.com/ubiquity-os-marketplace/command-start-stop/pull/86 + // ---------------------------------------------------------------- + it('Should retain arrays 1', () => { + const U = Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C'), Type.Literal('D')], { default: 'A' }) + const T = Type.Array(U, { default: ['A', 'B', 'C', 'D'], uniqueItems: true }) + Assert.IsEqual(Value.Default(T, undefined), ['A', 'B', 'C', 'D']) + Assert.IsEqual(Value.Default(T, []), []) + Assert.IsEqual(Value.Default(T, ['A']), ['A']) + // initialize undefined element + Assert.IsEqual(Value.Default(T, [undefined, 'B', 'C', 'D']), ['A', 'B', 'C', 'D']) + }) + it('Should retain arrays 2', () => { + const U = Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C'), Type.Literal('D')], { default: 'A' }) + // undefined first element initialized by union default + const T = Type.Array(U, { default: [undefined, 'B', 'C', 'D'], uniqueItems: true }) + Assert.IsEqual(Value.Default(T, undefined), ['A', 'B', 'C', 'D']) + }) +}) diff --git a/test/runtime/value/default/async-iterator.ts b/test/runtime/value/default/async-iterator.ts new file mode 100644 index 000000000..dd26f579c --- /dev/null +++ b/test/runtime/value/default/async-iterator.ts @@ -0,0 +1,16 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/default/AsyncIterator', () => { + it('Should use default', () => { + const T = Type.AsyncIterator(Type.Number(), { default: 1 }) + const R = Value.Default(T, undefined) + Assert.IsEqual(R, 1) + }) + it('Should use value', () => { + const T = Type.AsyncIterator(Type.Number(), { default: 1 }) + const R = Value.Default(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/default/bigint.ts b/test/runtime/value/default/bigint.ts new file mode 100644 index 000000000..b7e8aa710 --- /dev/null +++ b/test/runtime/value/default/bigint.ts @@ -0,0 +1,16 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/default/BigInt', () => { + it('Should use default', () => { + const T = Type.BigInt({ default: 1 }) + const R = Value.Default(T, undefined) + Assert.IsEqual(R, 1) + }) + it('Should use value', () => { + const T = Type.BigInt({ default: 1 }) + const R = Value.Default(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/default/boolean.ts b/test/runtime/value/default/boolean.ts new file mode 100644 index 000000000..ee05bb06b --- /dev/null +++ b/test/runtime/value/default/boolean.ts @@ -0,0 +1,16 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/default/Boolean', () => { + it('Should use default', () => { + const T = Type.Boolean({ default: 1 }) + const R = Value.Default(T, undefined) + Assert.IsEqual(R, 1) + }) + it('Should use value', () => { + const T = Type.Boolean({ default: 1 }) + const R = Value.Default(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/default/composite.ts b/test/runtime/value/default/composite.ts new file mode 100644 index 000000000..8a73e4106 --- /dev/null +++ b/test/runtime/value/default/composite.ts @@ -0,0 +1,16 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/default/Composite', () => { + it('Should use default', () => { + const T = Type.Composite([Type.Object({ x: Type.Number() }), Type.Object({ y: Type.Number() })], { default: 1 }) + const R = Value.Default(T, undefined) + Assert.IsEqual(R, 1) + }) + it('Should use value', () => { + const T = Type.Composite([Type.Object({ x: Type.Number() }), Type.Object({ y: Type.Number() })], { default: 1 }) + const R = Value.Default(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/default/constructor.ts b/test/runtime/value/default/constructor.ts new file mode 100644 index 000000000..76cb2343f --- /dev/null +++ b/test/runtime/value/default/constructor.ts @@ -0,0 +1,16 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/default/Constructor', () => { + it('Should use default', () => { + const T = Type.Constructor([], Type.Number(), { default: 1 }) + const R = Value.Default(T, undefined) + Assert.IsEqual(R, 1) + }) + it('Should use value', () => { + const T = Type.Constructor([], Type.Number(), { default: 1 }) + const R = Value.Default(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/default/date.ts b/test/runtime/value/default/date.ts new file mode 100644 index 000000000..72b95c0b4 --- /dev/null +++ b/test/runtime/value/default/date.ts @@ -0,0 +1,34 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/default/Date', () => { + it('Should use default', () => { + const T = Type.Date({ default: 1 }) + const R = Value.Default(T, undefined) + Assert.IsEqual(R, 1) + }) + it('Should use value', () => { + const T = Type.Date({ default: 1 }) + const R = Value.Default(T, null) + Assert.IsEqual(R, null) + }) + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/1024 + // ---------------------------------------------------------------- + it('Should use value if Date is valid', () => { + const T = Type.Date({ default: new Date(1) }) + const R = Value.Default(T, new Date(2)) as Date + Assert.IsEqual(R.getTime(), 2) + }) + it('Should use default if Date is undefined', () => { + const T = Type.Date({ default: new Date(1) }) + const R = Value.Default(T, undefined) as Date + Assert.IsEqual(R.getTime(), 1) + }) + it('Should use value if Date is invalid', () => { + const T = Type.Date({ default: new Date(1) }) + const R = Value.Default(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/default/enum.ts b/test/runtime/value/default/enum.ts new file mode 100644 index 000000000..ae6f9ece2 --- /dev/null +++ b/test/runtime/value/default/enum.ts @@ -0,0 +1,16 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/default/Enum', () => { + it('Should use default', () => { + const T = Type.Enum({}, { default: 1 }) + const R = Value.Default(T, undefined) + Assert.IsEqual(R, 1) + }) + it('Should use value', () => { + const T = Type.Enum({}, { default: 1 }) + const R = Value.Default(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/default/function.ts b/test/runtime/value/default/function.ts new file mode 100644 index 000000000..959c28284 --- /dev/null +++ b/test/runtime/value/default/function.ts @@ -0,0 +1,16 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/default/Function', () => { + it('Should use default', () => { + const T = Type.Function([], Type.Number(), { default: 1 }) + const R = Value.Default(T, undefined) + Assert.IsEqual(R, 1) + }) + it('Should use value', () => { + const T = Type.Function([], Type.Number(), { default: 1 }) + const R = Value.Default(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/default/import.ts b/test/runtime/value/default/import.ts new file mode 100644 index 000000000..42ae35672 --- /dev/null +++ b/test/runtime/value/default/import.ts @@ -0,0 +1,299 @@ +import { Value } from '@sinclair/typebox/value' +import { Type, CloneType } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/default/Import', () => { + it('Should use default', () => { + const T = Type.Module({ + A: Type.Object( + { + x: Type.Number(), + y: Type.Number(), + }, + { default: 1 }, + ), + }).Import('A') + const R = Value.Default(T, undefined) + Assert.IsEqual(R, 1) + }) + it('Should use value', () => { + const T = Type.Module({ + A: Type.Object( + { + x: Type.Number(), + y: Type.Number(), + }, + { default: 1 }, + ), + }).Import('A') + const R = Value.Default(T, null) + Assert.IsEqual(R, null) + }) + // ---------------------------------------------------------------- + // Construction + // ---------------------------------------------------------------- + it('Should should fully construct object 1', () => { + const T = Type.Module({ + A: Type.Object( + { + x: Type.Object( + { + x: Type.Number({ default: 1 }), + y: Type.Number({ default: 2 }), + }, + { default: {} }, + ), + y: Type.Object( + { + x: Type.Number({ default: 3 }), + y: Type.Number({ default: 4 }), + }, + { default: {} }, + ), + }, + { default: {} }, + ), + }).Import('A') + const R = Value.Default(T, undefined) + Assert.IsEqual(R, { x: { x: 1, y: 2 }, y: { x: 3, y: 4 } }) + }) + it('Should should fully construct object 2', () => { + const T = Type.Module({ + A: Type.Object( + { + x: Type.Object( + { + x: Type.Number({ default: 1 }), + y: Type.Number({ default: 2 }), + }, + { default: {} }, + ), + y: Type.Object( + { + x: Type.Number({ default: 3 }), + y: Type.Number({ default: 4 }), + }, + { default: {} }, + ), + }, + { default: {} }, + ), + }).Import('A') + const R = Value.Default(T, { x: null }) + Assert.IsEqual(R, { x: null, y: { x: 3, y: 4 } }) + }) + it('Should should fully construct object 3', () => { + const T = Type.Module({ + A: Type.Object( + { + x: Type.Object( + { + x: Type.Number({ default: 1 }), + y: Type.Number({ default: 2 }), + }, + { default: {} }, + ), + y: Type.Object( + { + x: Type.Number({ default: 3 }), + y: Type.Number({ default: 4 }), + }, + { default: {} }, + ), + }, + { default: {} }, + ), + }).Import('A') + const R = Value.Default(T, { x: { x: null, y: null } }) + Assert.IsEqual(R, { x: { x: null, y: null }, y: { x: 3, y: 4 } }) + }) + // ---------------------------------------------------------------- + // Properties + // ---------------------------------------------------------------- + it('Should use property defaults 1', () => { + const T = Type.Module({ + A: Type.Object( + { + x: Type.Number({ default: 1 }), + y: Type.Number({ default: 2 }), + }, + { default: 1 }, + ), + }).Import('A') + const R = Value.Default(T, {}) + Assert.IsEqual(R, { x: 1, y: 2 }) + }) + it('Should use property defaults 2', () => { + const T = Type.Module({ + A: Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + }).Import('A') + const R = Value.Default(T, {}) + Assert.IsEqual(R, {}) + }) + it('Should use property defaults 3', () => { + const T = Type.Module({ + A: Type.Object({ + x: Type.Number({ default: 1 }), + y: Type.Number(), + }), + }).Import('A') + const R = Value.Default(T, {}) + Assert.IsEqual(R, { x: 1 }) + }) + it('Should use property defaults 4', () => { + const T = Type.Module({ + A: Type.Object({ + x: Type.Number({ default: 1 }), + y: Type.Number(), + }), + }).Import('A') + const R = Value.Default(T, { x: 3 }) + Assert.IsEqual(R, { x: 3 }) + }) + // ---------------------------------------------------------------- + // AdditionalProperties + // ---------------------------------------------------------------- + it('Should use additional property defaults 1', () => { + const T = Type.Module({ + A: Type.Object( + { + x: Type.Number({ default: 1 }), + y: Type.Number({ default: 2 }), + }, + { + additionalProperties: Type.Number({ default: 3 }), + }, + ), + }).Import('A') + const R = Value.Default(T, {}) + Assert.IsEqual(R, { x: 1, y: 2 }) + }) + it('Should use additional property defaults 2', () => { + const T = Type.Module({ + A: Type.Object( + { + x: Type.Number({ default: 1 }), + y: Type.Number({ default: 2 }), + }, + { + additionalProperties: Type.Number({ default: 3 }), + }, + ), + }).Import('A') + const R = Value.Default(T, { x: null, y: null, z: undefined }) + Assert.IsEqual(R, { x: null, y: null, z: 3 }) + }) + it('Should use additional property defaults 3', () => { + const T = Type.Module({ + A: Type.Object( + { + x: Type.Number({ default: 1 }), + y: Type.Number({ default: 2 }), + }, + { + additionalProperties: Type.Number(), + }, + ), + }).Import('A') + const R = Value.Default(T, { x: null, y: null, z: undefined }) + Assert.IsEqual(R, { x: null, y: null, z: undefined }) + }) + // ---------------------------------------------------------------- + // Mutation + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/726 + it('Should retain defaults on operation', () => { + const A = Type.Module({ + A: Type.Object({ + a: Type.Object( + { + b: Type.Array(Type.String(), { default: [] }), + }, + { default: {} }, + ), + }), + }).Import('A') + const value = Value.Default(A, {}) + Assert.IsEqual(value, { a: { b: [] } }) + Assert.IsEqual(A.$defs.A.properties.a.default, {}) + Assert.IsEqual(A.$defs.A.properties.a.properties.b.default, []) + }) + // https://github.com/sinclairzx81/typebox/issues/726 + it('Should retain schematics on operation', () => { + const A = Type.Module({ + A: Type.Object({ + a: Type.Object( + { + b: Type.Array(Type.String(), { default: [] }), + }, + { default: {} }, + ), + }), + }).Import('A') + const B = CloneType(A) + Value.Default(A, {}) + Assert.IsEqual(A, B) + }) + // ---------------------------------------------------------------- + // Traveral: https://github.com/sinclairzx81/typebox/issues/962 + // ---------------------------------------------------------------- + it('Should traverse into an object 1 (initialize)', () => { + const M = Type.Module({ + Y: Type.Object({ y: Type.String({ default: 'y' }) }), + X: Type.Object({ x: Type.Ref('Y') }), + }) + const Y = M.Import('Y') + const X = M.Import('X') + Assert.IsEqual(Value.Default(Y, {}), { y: 'y' }) + Assert.IsEqual(Value.Default(X, { x: {} }), { x: { y: 'y' } }) + }) + it('Should traverse into an object 2 (retain)', () => { + const M = Type.Module({ + Y: Type.Object({ y: Type.String({ default: 'y' }) }), + X: Type.Object({ x: Type.Ref('Y') }), + }) + const Y = M.Import('Y') + const X = M.Import('X') + Assert.IsEqual(Value.Default(X, { x: { y: 1 } }), { x: { y: 1 } }) + }) + it('Should traverse into an object 3 (ignore on undefined)', () => { + const M = Type.Module({ + Y: Type.Object({ y: Type.String({ default: 'y' }) }), + X: Type.Object({ x: Type.Ref('Y') }), + }) + const Y = M.Import('Y') + const X = M.Import('X') + Assert.IsEqual(Value.Default(X, { x: undefined }), { x: undefined }) + }) + // ---------------------------------------------------------------- + // Exterior Object Defaults + // ---------------------------------------------------------------- + it('Should default exterior into an object 1', () => { + const X = Type.Module({ A: Type.Object({ x: Type.String({ default: 1 }) }, { default: {} }) }).Import('A') + const R = Value.Default(X, undefined) + Assert.IsEqual(R, { x: 1 }) + }) + it('Should default exterior into an object 2', () => { + const X = Type.Module({ A: Type.Object({ x: Type.String({ default: 1 }) }, { default: {} }) }).Import('A') + const R = Value.Default(X, {}) + Assert.IsEqual(R, { x: 1 }) + }) + it('Should default exterior into an object 3', () => { + const X = Type.Module({ A: Type.Object({ x: Type.String({ default: 1 }) }, { default: {} }) }).Import('A') + const R = Value.Default(X, { y: 3 }) + Assert.IsEqual(R, { y: 3, x: 1 }) + }) + it('Should default exterior into an object 4', () => { + const X = Type.Module({ A: Type.Object({ x: Type.String({ default: 1 }) }, { default: {} }) }).Import('A') + const R = Value.Default(X, { y: 3, x: 7 }) + Assert.IsEqual(R, { y: 3, x: 7 }) + }) + it('Should default exterior into an object 5', () => { + const X = Type.Module({ A: Type.Object({ x: Type.String({ default: 1 }) }, { default: {} }) }).Import('A') + const R = Value.Default(X, { x: 2 }) + Assert.IsEqual(R, { x: 2 }) + }) +}) diff --git a/test/runtime/value/default/index.ts b/test/runtime/value/default/index.ts new file mode 100644 index 000000000..fd703993c --- /dev/null +++ b/test/runtime/value/default/index.ts @@ -0,0 +1,37 @@ +import './_deferred' +import './any' +import './array' +import './async-iterator' +import './bigint' +import './boolean' +import './composite' +import './constructor' +import './date' +import './enum' +import './function' +import './import' +import './integer' +import './intersect' +import './iterator' +import './keyof' +import './kind' +import './literal' +import './never' +import './not' +import './null' +import './number' +import './object' +import './promise' +import './record' +import './recursive' +import './ref' +import './regexp' +import './string' +import './symbol' +import './template-literal' +import './tuple' +import './uint8array' +import './undefined' +import './union' +import './unknown' +import './void' diff --git a/test/runtime/value/default/integer.ts b/test/runtime/value/default/integer.ts new file mode 100644 index 000000000..c0ce64b4a --- /dev/null +++ b/test/runtime/value/default/integer.ts @@ -0,0 +1,16 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/default/Integer', () => { + it('Should use default', () => { + const T = Type.Integer({ default: 1 }) + const R = Value.Default(T, undefined) + Assert.IsEqual(R, 1) + }) + it('Should use value', () => { + const T = Type.Integer({ default: 1 }) + const R = Value.Default(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/default/intersect.ts b/test/runtime/value/default/intersect.ts new file mode 100644 index 000000000..0e72b3e6e --- /dev/null +++ b/test/runtime/value/default/intersect.ts @@ -0,0 +1,106 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/default/Intersect', () => { + it('Should use default', () => { + const T = Type.Intersect([Type.Number(), Type.String()], { default: 1 }) + const R = Value.Default(T, undefined) + Assert.IsEqual(R, 1) + }) + it('Should use value', () => { + const T = Type.Intersect([Type.Number(), Type.String()], { default: 1 }) + const R = Value.Default(T, null) + Assert.IsEqual(R, null) + }) + // ---------------------------------------------------------------- + // Intersected + // ---------------------------------------------------------------- + it('Should use default intersected 1', () => { + const A = Type.Object({ + a: Type.Number({ default: 1 }), + }) + const B = Type.Object({ + b: Type.Number({ default: 2 }), + }) + const T = Type.Intersect([A, B]) + const R = Value.Default(T, {}) + Assert.IsEqual(R, { a: 1, b: 2 }) + }) + it('Should use default intersected 2', () => { + const A = Type.Object({ + a: Type.Number(), + }) + const B = Type.Object({ + b: Type.Number(), + }) + const T = Type.Intersect([A, B]) + const R = Value.Default(T, {}) + Assert.IsEqual(R, {}) + }) + it('Should use default intersected 3', () => { + const A = Type.Object({ + a: Type.Number({ default: 1 }), + }) + const B = Type.Object({ + b: Type.Number({ default: 2 }), + }) + const T = Type.Intersect([A, B]) + const R = Value.Default(T, { a: 3 }) + Assert.IsEqual(R, { a: 3, b: 2 }) + }) + it('Should use default intersected 4', () => { + const A = Type.Object({ + a: Type.Number({ default: 1 }), + }) + const B = Type.Object({ + b: Type.Number({ default: 2 }), + }) + const T = Type.Intersect([A, B]) + const R = Value.Default(T, { a: 4, b: 5 }) + Assert.IsEqual(R, { a: 4, b: 5 }) + }) + it('Should use default intersected 5', () => { + const A = Type.Object({ + a: Type.Number({ default: 1 }), + }) + const B = Type.Number({ default: 2 }) + const T = Type.Intersect([A, B]) + const R = Value.Default(T, {}) + Assert.IsEqual(R, { a: 1 }) + }) + it('Should use default intersected 6', () => { + const A = Type.Number({ default: 2 }) + const B = Type.Object({ + a: Type.Number({ default: 1 }), + }) + const T = Type.Intersect([A, B]) + const R = Value.Default(T, {}) + Assert.IsEqual(R, { a: 1 }) + }) + // ---------------------------------------------------------------- + // Intersected Deep + // ---------------------------------------------------------------- + it('Should use default intersected deep 1', () => { + const A = Type.Object({ a: Type.Number({ default: 1 }) }) + const B = Type.Object({ b: Type.Number({ default: 2 }) }) + const C = Type.Object({ c: Type.Number({ default: 3 }) }) + const D = Type.Object({ d: Type.Number({ default: 4 }) }) + const T1 = Type.Intersect([A, B]) + const T2 = Type.Intersect([C, D]) + const T = Type.Intersect([T1, T2]) + const R = Value.Default(T, {}) + Assert.IsEqual(R, { a: 1, b: 2, c: 3, d: 4 }) + }) + it('Should use default intersected deep 2', () => { + const A = Type.Object({ a: Type.Number({}) }) + const B = Type.Object({ b: Type.Number({}) }) + const C = Type.Object({ c: Type.Number({}) }) + const D = Type.Object({ d: Type.Number({}) }) + const T1 = Type.Intersect([A, B]) + const T2 = Type.Intersect([C, D]) + const T = Type.Intersect([T1, T2]) + const R = Value.Default(T, {}) + Assert.IsEqual(R, {}) + }) +}) diff --git a/test/runtime/value/default/iterator.ts b/test/runtime/value/default/iterator.ts new file mode 100644 index 000000000..d49b7f296 --- /dev/null +++ b/test/runtime/value/default/iterator.ts @@ -0,0 +1,16 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/default/Iterator', () => { + it('Should use default', () => { + const T = Type.Iterator(Type.Number(), { default: 1 }) + const R = Value.Default(T, undefined) + Assert.IsEqual(R, 1) + }) + it('Should use value', () => { + const T = Type.Iterator(Type.Number(), { default: 1 }) + const R = Value.Default(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/default/keyof.ts b/test/runtime/value/default/keyof.ts new file mode 100644 index 000000000..2cb0bb26b --- /dev/null +++ b/test/runtime/value/default/keyof.ts @@ -0,0 +1,16 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/default/KeyOf', () => { + it('Should use default', () => { + const T = Type.KeyOf(Type.Object({ x: Type.Number() }), { default: 1 }) + const R = Value.Default(T, undefined) + Assert.IsEqual(R, 1) + }) + it('Should use value', () => { + const T = Type.KeyOf(Type.Object({ x: Type.Number() }), { default: 1 }) + const R = Value.Default(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/default/kind.ts b/test/runtime/value/default/kind.ts new file mode 100644 index 000000000..1f8a96fcc --- /dev/null +++ b/test/runtime/value/default/kind.ts @@ -0,0 +1,16 @@ +import { Value } from '@sinclair/typebox/value' +import { Type, Kind } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/default/Kind', () => { + it('Should use default', () => { + const T = Type.Unsafe({ [Kind]: 'Unknown', default: 1 }) + const R = Value.Default(T, undefined) + Assert.IsEqual(R, 1) + }) + it('Should use value', () => { + const T = Type.Unsafe({ [Kind]: 'Unknown', default: 1 }) + const R = Value.Default(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/default/literal.ts b/test/runtime/value/default/literal.ts new file mode 100644 index 000000000..fa01d55b9 --- /dev/null +++ b/test/runtime/value/default/literal.ts @@ -0,0 +1,16 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/default/Literal', () => { + it('Should use default', () => { + const T = Type.Literal('foo', { default: 1 }) + const R = Value.Default(T, undefined) + Assert.IsEqual(R, 1) + }) + it('Should use value', () => { + const T = Type.Literal('foo', { default: 1 }) + const R = Value.Default(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/default/never.ts b/test/runtime/value/default/never.ts new file mode 100644 index 000000000..a39542739 --- /dev/null +++ b/test/runtime/value/default/never.ts @@ -0,0 +1,16 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/default/Never', () => { + it('Should use default', () => { + const T = Type.Never({ default: 1 }) + const R = Value.Default(T, undefined) + Assert.IsEqual(R, 1) + }) + it('Should use value', () => { + const T = Type.Never({ default: 1 }) + const R = Value.Default(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/default/not.ts b/test/runtime/value/default/not.ts new file mode 100644 index 000000000..148756d5b --- /dev/null +++ b/test/runtime/value/default/not.ts @@ -0,0 +1,16 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/default/Not', () => { + it('Should use default', () => { + const T = Type.Not(Type.String(), { default: 1 }) + const R = Value.Default(T, undefined) + Assert.IsEqual(R, 1) + }) + it('Should use value', () => { + const T = Type.Not(Type.String(), { default: 1 }) + const R = Value.Default(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/default/null.ts b/test/runtime/value/default/null.ts new file mode 100644 index 000000000..9246fe893 --- /dev/null +++ b/test/runtime/value/default/null.ts @@ -0,0 +1,16 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/default/Null', () => { + it('Should use default', () => { + const T = Type.Null({ default: 1 }) + const R = Value.Default(T, undefined) + Assert.IsEqual(R, 1) + }) + it('Should use value', () => { + const T = Type.Null({ default: 1 }) + const R = Value.Default(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/default/number.ts b/test/runtime/value/default/number.ts new file mode 100644 index 000000000..6dcaa7035 --- /dev/null +++ b/test/runtime/value/default/number.ts @@ -0,0 +1,16 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/default/Number', () => { + it('Should use default', () => { + const T = Type.Number({ default: 1 }) + const R = Value.Default(T, undefined) + Assert.IsEqual(R, 1) + }) + it('Should use value', () => { + const T = Type.Number({ default: 1 }) + const R = Value.Default(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/default/object.ts b/test/runtime/value/default/object.ts new file mode 100644 index 000000000..86a7df19f --- /dev/null +++ b/test/runtime/value/default/object.ts @@ -0,0 +1,259 @@ +import { Value } from '@sinclair/typebox/value' +import { Type, CloneType } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/default/Object', () => { + it('Should use default', () => { + const T = Type.Object( + { + x: Type.Number(), + y: Type.Number(), + }, + { default: 1 }, + ) + const R = Value.Default(T, undefined) + Assert.IsEqual(R, 1) + }) + it('Should use value', () => { + const T = Type.Object( + { + x: Type.Number(), + y: Type.Number(), + }, + { default: 1 }, + ) + const R = Value.Default(T, null) + Assert.IsEqual(R, null) + }) + // ---------------------------------------------------------------- + // Construction + // ---------------------------------------------------------------- + it('Should should fully construct object 1', () => { + const T = Type.Object( + { + x: Type.Object( + { + x: Type.Number({ default: 1 }), + y: Type.Number({ default: 2 }), + }, + { default: {} }, + ), + y: Type.Object( + { + x: Type.Number({ default: 3 }), + y: Type.Number({ default: 4 }), + }, + { default: {} }, + ), + }, + { default: {} }, + ) + const R = Value.Default(T, undefined) + Assert.IsEqual(R, { x: { x: 1, y: 2 }, y: { x: 3, y: 4 } }) + }) + it('Should should fully construct object 2', () => { + const T = Type.Object( + { + x: Type.Object( + { + x: Type.Number({ default: 1 }), + y: Type.Number({ default: 2 }), + }, + { default: {} }, + ), + y: Type.Object( + { + x: Type.Number({ default: 3 }), + y: Type.Number({ default: 4 }), + }, + { default: {} }, + ), + }, + { default: {} }, + ) + const R = Value.Default(T, { x: null }) + Assert.IsEqual(R, { x: null, y: { x: 3, y: 4 } }) + }) + it('Should should fully construct object 3', () => { + const T = Type.Object( + { + x: Type.Object( + { + x: Type.Number({ default: 1 }), + y: Type.Number({ default: 2 }), + }, + { default: {} }, + ), + y: Type.Object( + { + x: Type.Number({ default: 3 }), + y: Type.Number({ default: 4 }), + }, + { default: {} }, + ), + }, + { default: {} }, + ) + const R = Value.Default(T, { x: { x: null, y: null } }) + Assert.IsEqual(R, { x: { x: null, y: null }, y: { x: 3, y: 4 } }) + }) + // ---------------------------------------------------------------- + // Properties + // ---------------------------------------------------------------- + it('Should use property defaults 1', () => { + const T = Type.Object( + { + x: Type.Number({ default: 1 }), + y: Type.Number({ default: 2 }), + }, + { default: 1 }, + ) + const R = Value.Default(T, {}) + Assert.IsEqual(R, { x: 1, y: 2 }) + }) + it('Should use property defaults 2', () => { + const T = Type.Object({ + x: Type.Number(), + y: Type.Number(), + }) + const R = Value.Default(T, {}) + Assert.IsEqual(R, {}) + }) + it('Should use property defaults 3', () => { + const T = Type.Object({ + x: Type.Number({ default: 1 }), + y: Type.Number(), + }) + const R = Value.Default(T, {}) + Assert.IsEqual(R, { x: 1 }) + }) + it('Should use property defaults 4', () => { + const T = Type.Object({ + x: Type.Number({ default: 1 }), + y: Type.Number(), + }) + const R = Value.Default(T, { x: 3 }) + Assert.IsEqual(R, { x: 3 }) + }) + // ---------------------------------------------------------------- + // AdditionalProperties + // ---------------------------------------------------------------- + it('Should use additional property defaults 1', () => { + const T = Type.Object( + { + x: Type.Number({ default: 1 }), + y: Type.Number({ default: 2 }), + }, + { + additionalProperties: Type.Number({ default: 3 }), + }, + ) + const R = Value.Default(T, {}) + Assert.IsEqual(R, { x: 1, y: 2 }) + }) + it('Should use additional property defaults 2', () => { + const T = Type.Object( + { + x: Type.Number({ default: 1 }), + y: Type.Number({ default: 2 }), + }, + { + additionalProperties: Type.Number({ default: 3 }), + }, + ) + const R = Value.Default(T, { x: null, y: null, z: undefined }) + Assert.IsEqual(R, { x: null, y: null, z: 3 }) + }) + it('Should use additional property defaults 3', () => { + const T = Type.Object( + { + x: Type.Number({ default: 1 }), + y: Type.Number({ default: 2 }), + }, + { + additionalProperties: Type.Number(), + }, + ) + const R = Value.Default(T, { x: null, y: null, z: undefined }) + Assert.IsEqual(R, { x: null, y: null, z: undefined }) + }) + // ---------------------------------------------------------------- + // Mutation + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/726 + it('Should retain defaults on operation', () => { + const A = Type.Object({ + a: Type.Object( + { + b: Type.Array(Type.String(), { default: [] }), + }, + { default: {} }, + ), + }) + const value = Value.Default(A, {}) + Assert.IsEqual(value, { a: { b: [] } }) + Assert.IsEqual(A.properties.a.default, {}) + Assert.IsEqual(A.properties.a.properties.b.default, []) + }) + // https://github.com/sinclairzx81/typebox/issues/726 + it('Should retain schematics on operation', () => { + const A = Type.Object({ + a: Type.Object( + { + b: Type.Array(Type.String(), { default: [] }), + }, + { default: {} }, + ), + }) + const B = CloneType(A) + Value.Default(A, {}) + Assert.IsEqual(A, B) + }) + // ---------------------------------------------------------------- + // Traveral: https://github.com/sinclairzx81/typebox/issues/962 + // ---------------------------------------------------------------- + it('Should traverse into an object 1 (initialize)', () => { + const Y = Type.Object({ y: Type.String({ default: 'y' }) }) + const X = Type.Object({ x: Y }) + Assert.IsEqual(Value.Default(Y, {}), { y: 'y' }) + Assert.IsEqual(Value.Default(X, { x: {} }), { x: { y: 'y' } }) + }) + it('Should traverse into an object 2 (retain)', () => { + const Y = Type.Object({ y: Type.String({ default: 'y' }) }) + const X = Type.Object({ x: Y }) + Assert.IsEqual(Value.Default(X, { x: { y: 1 } }), { x: { y: 1 } }) + }) + it('Should traverse into an object 3 (ignore on undefined)', () => { + const Y = Type.Object({ y: Type.String({ default: 'y' }) }) + const X = Type.Object({ x: Y }) + Assert.IsEqual(Value.Default(X, { x: undefined }), { x: undefined }) + }) + // ---------------------------------------------------------------- + // Exterior Object Defaults + // ---------------------------------------------------------------- + it('Should default exterior into an object 1', () => { + const X = Type.Object({ x: Type.String({ default: 1 }) }, { default: {} }) + const R = Value.Default(X, undefined) + Assert.IsEqual(R, { x: 1 }) + }) + it('Should default exterior into an object 2', () => { + const X = Type.Object({ x: Type.String({ default: 1 }) }, { default: {} }) + const R = Value.Default(X, {}) + Assert.IsEqual(R, { x: 1 }) + }) + it('Should default exterior into an object 3', () => { + const X = Type.Object({ x: Type.String({ default: 1 }) }, { default: {} }) + const R = Value.Default(X, { y: 3 }) + Assert.IsEqual(R, { y: 3, x: 1 }) + }) + it('Should default exterior into an object 4', () => { + const X = Type.Object({ x: Type.String({ default: 1 }) }, { default: {} }) + const R = Value.Default(X, { y: 3, x: 7 }) + Assert.IsEqual(R, { y: 3, x: 7 }) + }) + it('Should default exterior into an object 5', () => { + const X = Type.Object({ x: Type.String({ default: 1 }) }, { default: {} }) + const R = Value.Default(X, { x: 2 }) + Assert.IsEqual(R, { x: 2 }) + }) +}) diff --git a/test/runtime/value/default/promise.ts b/test/runtime/value/default/promise.ts new file mode 100644 index 000000000..659e052cb --- /dev/null +++ b/test/runtime/value/default/promise.ts @@ -0,0 +1,16 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/default/Promise', () => { + it('Should use default', () => { + const T = Type.Any({ default: 1 }) + const R = Value.Default(T, undefined) + Assert.IsEqual(R, 1) + }) + it('Should use value', () => { + const T = Type.Any({ default: 1 }) + const R = Value.Default(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/default/record.ts b/test/runtime/value/default/record.ts new file mode 100644 index 000000000..4b300ddc2 --- /dev/null +++ b/test/runtime/value/default/record.ts @@ -0,0 +1,66 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/default/Record', () => { + it('Should use default', () => { + const T = Type.Record(Type.String(), Type.Number(), { default: 1 }) + const R = Value.Default(T, undefined) + Assert.IsEqual(R, 1) + }) + it('Should use value', () => { + const T = Type.Record(Type.String(), Type.Number(), { default: 1 }) + const R = Value.Default(T, null) + Assert.IsEqual(R, null) + }) + // // ---------------------------------------------------------------- + // // Properties + // // ---------------------------------------------------------------- + it('Should use property defaults 1', () => { + const T = Type.Record(Type.Number(), Type.Number({ default: 1 })) + const R = Value.Default(T, { 0: undefined }) + Assert.IsEqual(R, { 0: 1 }) + }) + it('Should use property defaults 2', () => { + const T = Type.Record(Type.Number(), Type.Number({ default: 1 })) + const R = Value.Default(T, { 0: null }) + Assert.IsEqual(R, { 0: null }) + }) + it('Should use property defaults 3', () => { + const T = Type.Record(Type.Number(), Type.Number({ default: 1 })) + const R = Value.Default(T, { a: undefined }) + Assert.IsEqual(R, { a: undefined }) + }) + it('Should use property defaults 4', () => { + const T = Type.Record(Type.Number(), Type.Number({ default: 1 })) + const R = Value.Default(T, { 0: undefined }) + Assert.IsEqual(R, { 0: 1 }) + }) + it('Should use property defaults 5', () => { + const T = Type.Record(Type.Number(), Type.Number()) + const R = Value.Default(T, { 0: undefined }) + Assert.IsEqual(R, { 0: undefined }) + }) + it('Should use property defaults 6', () => { + const T = Type.Record(Type.Number(), Type.Number({ default: 1 })) + const R = Value.Default(T, {}) + Assert.IsEqual(R, {}) + }) + // ---------------------------------------------------------------- + // Additional Properties + // ---------------------------------------------------------------- + it('Should use additional property defaults 1', () => { + const T = Type.Record(Type.Number(), Type.Number({ default: 1 }), { + additionalProperties: Type.Number({ default: 3 }), + }) + const R = Value.Default(T, { 0: undefined, a: undefined }) + Assert.IsEqual(R, { 0: 1, a: 3 }) + }) + it('Should use additional property defaults 2', () => { + const T = Type.Record(Type.Number(), Type.Number({ default: 1 }), { + additionalProperties: Type.Number(), + }) + const R = Value.Default(T, { 0: undefined, a: undefined }) + Assert.IsEqual(R, { 0: 1, a: undefined }) + }) +}) diff --git a/test/runtime/value/default/recursive.ts b/test/runtime/value/default/recursive.ts new file mode 100644 index 000000000..492f92d24 --- /dev/null +++ b/test/runtime/value/default/recursive.ts @@ -0,0 +1,93 @@ +import { Value } from '@sinclair/typebox/value' +import { TSchema, Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +// prettier-ignore +describe('value/default/Recursive', () => { + it('Should use default', () => { + const T = Type.Recursive((This) => Type.Object({ + nodes: Type.Array(This) + }, { default: 1 })) + const R = Value.Default(T, undefined) + Assert.IsEqual(R, 1) + }) + it('Should use value', () => { + const T = Type.Recursive((This) => Type.Object({ + nodes: Type.Array(This) + }, { default: 1 })) + const R = Value.Default(T, null) + Assert.IsEqual(R, null) + }) + // ---------------------------------------------------------------- + // Recursive + // ---------------------------------------------------------------- + it('Should use default recursive values', () => { + const T = Type.Recursive((This) => Type.Object({ + id: Type.String({ default: 1 }), + nodes: Type.Array(This, { default: [] }) // need this + })) + const R = Value.Default(T, { + nodes: [{ + nodes: [{ + nodes: [{ id: null }] + }, { + nodes: [{ id: null }] + }] + }] + }) + Assert.IsEqual(R, { + nodes: [{ + nodes: [{ + nodes: [{ + id: null, + nodes: [] + }], + id: 1 + }, { + nodes: [{ + id: null, + nodes: [] + }], + id: 1 + }], + id: 1 + }], + id: 1 + }) + }) + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/1010 + // ---------------------------------------------------------------- + it('Should default Recursive Union', () => { + const Binary = (node: Node) => Type.Object({ + type: Type.Literal('Binary'), + left: node, + right: node + }) + const Node = Type.Object({ + type: Type.Literal('Node'), + value: Type.String({ default: 'X' }) + }) + const Expr = Type.Recursive(This => Type.Union([Binary(This), Node])) + const R = Value.Default(Expr, { + type: 'Binary', + left: { + type: 'Node' + }, + right: { + type: 'Node' + } + }) + Assert.IsEqual(R, { + type: 'Binary', + left: { + type: 'Node', + value: 'X' + }, + right: { + type: 'Node', + value: 'X' + } + }) + }) +}) diff --git a/test/runtime/value/default/ref.ts b/test/runtime/value/default/ref.ts new file mode 100644 index 000000000..634059573 --- /dev/null +++ b/test/runtime/value/default/ref.ts @@ -0,0 +1,33 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/default/Ref', () => { + it('Should use default', () => { + const A = Type.String({ $id: 'A' }) + const T = Type.Ref('A', { default: 1 }) + const R = Value.Default(T, [A], undefined) + Assert.IsEqual(R, 1) + }) + it('Should use value', () => { + const A = Type.String({ $id: 'A' }) + const T = Type.Ref('A', { default: 1 }) + const R = Value.Default(T, [A], null) + Assert.IsEqual(R, null) + }) + // ---------------------------------------------------------------- + // Foreign + // ---------------------------------------------------------------- + it('Should use default on foreign value', () => { + const A = Type.String({ $id: 'A', default: 1 }) + const T = Type.Ref('A') + const R = Value.Default(T, [A], undefined) + Assert.IsEqual(R, 1) + }) + it('Should use value on foreign value', () => { + const A = Type.String({ $id: 'A', default: 1 }) + const T = Type.Ref('A') + const R = Value.Default(T, [A], null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/default/regexp.ts b/test/runtime/value/default/regexp.ts new file mode 100644 index 000000000..9c32c7ab2 --- /dev/null +++ b/test/runtime/value/default/regexp.ts @@ -0,0 +1,16 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/default/RegExp', () => { + it('Should use default', () => { + const T = Type.RegExp('', { default: 1 }) + const R = Value.Default(T, undefined) + Assert.IsEqual(R, 1) + }) + it('Should use value', () => { + const T = Type.RegExp('', { default: 1 }) + const R = Value.Default(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/default/string.ts b/test/runtime/value/default/string.ts new file mode 100644 index 000000000..af833332e --- /dev/null +++ b/test/runtime/value/default/string.ts @@ -0,0 +1,16 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/default/String', () => { + it('Should use default', () => { + const T = Type.String({ default: 1 }) + const R = Value.Default(T, undefined) + Assert.IsEqual(R, 1) + }) + it('Should use value', () => { + const T = Type.String({ default: 1 }) + const R = Value.Default(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/default/symbol.ts b/test/runtime/value/default/symbol.ts new file mode 100644 index 000000000..d593fb7b8 --- /dev/null +++ b/test/runtime/value/default/symbol.ts @@ -0,0 +1,16 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/default/Symbol', () => { + it('Should use default', () => { + const T = Type.Symbol({ default: 1 }) + const R = Value.Default(T, undefined) + Assert.IsEqual(R, 1) + }) + it('Should use value', () => { + const T = Type.Symbol({ default: 1 }) + const R = Value.Default(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/default/template-literal.ts b/test/runtime/value/default/template-literal.ts new file mode 100644 index 000000000..2dd6b7e83 --- /dev/null +++ b/test/runtime/value/default/template-literal.ts @@ -0,0 +1,16 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/default/TemplateLiteral', () => { + it('Should use default', () => { + const T = Type.TemplateLiteral('hello', { default: 1 }) + const R = Value.Default(T, undefined) + Assert.IsEqual(R, 1) + }) + it('Should use value', () => { + const T = Type.TemplateLiteral('hello', { default: 1 }) + const R = Value.Default(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/default/tuple.ts b/test/runtime/value/default/tuple.ts new file mode 100644 index 000000000..c7acdc99b --- /dev/null +++ b/test/runtime/value/default/tuple.ts @@ -0,0 +1,44 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/default/Tuple', () => { + it('Should use default', () => { + const T = Type.Tuple([Type.Number(), Type.Number()], { default: 1 }) + const R = Value.Default(T, undefined) + Assert.IsEqual(R, 1) + }) + it('Should use value', () => { + const T = Type.Tuple([Type.Number(), Type.Number()], { default: 1 }) + const R = Value.Default(T, null) + Assert.IsEqual(R, null) + }) + // ---------------------------------------------------------------- + // Elements + // ---------------------------------------------------------------- + it('Should use default elements 1', () => { + const T = Type.Tuple([Type.Number({ default: 1 }), Type.Number({ default: 2 })], { default: [] }) + const R = Value.Default(T, null) + Assert.IsEqual(R, null) + }) + it('Should use default elements 2', () => { + const T = Type.Tuple([Type.Number({ default: 1 }), Type.Number({ default: 2 })], { default: [] }) + const R = Value.Default(T, undefined) + Assert.IsEqual(R, [1, 2]) + }) + it('Should use default elements 3', () => { + const T = Type.Tuple([Type.Number({ default: 1 }), Type.Number({ default: 2 })], { default: [] }) + const R = Value.Default(T, [4, 5, 6]) + Assert.IsEqual(R, [4, 5, 6]) + }) + it('Should use default elements 4', () => { + const T = Type.Tuple([Type.Number({ default: 1 }), Type.Number({ default: 2 })]) + const R = Value.Default(T, [4, 5, 6]) + Assert.IsEqual(R, [4, 5, 6]) + }) + it('Should use default elements 5', () => { + const T = Type.Tuple([Type.Number({ default: 1 }), Type.Number({ default: 2 })]) + const R = Value.Default(T, [4]) + Assert.IsEqual(R, [4, 2]) + }) +}) diff --git a/test/runtime/value/default/uint8array.ts b/test/runtime/value/default/uint8array.ts new file mode 100644 index 000000000..5c3cfa77d --- /dev/null +++ b/test/runtime/value/default/uint8array.ts @@ -0,0 +1,16 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/default/Uint8Array', () => { + it('Should use default', () => { + const T = Type.Uint8Array({ default: 1 }) + const R = Value.Default(T, undefined) + Assert.IsEqual(R, 1) + }) + it('Should use value', () => { + const T = Type.Uint8Array({ default: 1 }) + const R = Value.Default(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/default/undefined.ts b/test/runtime/value/default/undefined.ts new file mode 100644 index 000000000..d622cd1c6 --- /dev/null +++ b/test/runtime/value/default/undefined.ts @@ -0,0 +1,16 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/default/Undefined', () => { + it('Should use default', () => { + const T = Type.Undefined({ default: 1 }) + const R = Value.Default(T, undefined) + Assert.IsEqual(R, 1) + }) + it('Should use value', () => { + const T = Type.Undefined({ default: 1 }) + const R = Value.Default(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/default/union.ts b/test/runtime/value/default/union.ts new file mode 100644 index 000000000..15b3512fc --- /dev/null +++ b/test/runtime/value/default/union.ts @@ -0,0 +1,127 @@ +import { Value } from '@sinclair/typebox/value' +import { Type, Kind, TypeRegistry } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +// prettier-ignore +describe('value/default/Union', () => { + it('Should use default', () => { + const T = Type.Union([Type.Number(), Type.String()], { default: 1 }) + const R = Value.Default(T, undefined) + Assert.IsEqual(R, 1) + }) + it('Should use value', () => { + const T = Type.Union([Type.Number(), Type.String()], { default: 1 }) + const R = Value.Default(T, null) + Assert.IsEqual(R, null) + }) + // ---------------------------------------------------------------- + // Interior + // ---------------------------------------------------------------- + it('Should default interior 1', () => { + const T = Type.Union([ + Type.Object({ + x: Type.Number({ default: 1 }), + y: Type.Number({ default: 2 }), + }), + Type.String({ default: 'hello' }), + ]) + const R = Value.Default(T, null) + Assert.IsEqual(R, null) + }) + it('Should default interior 2', () => { + const T = Type.Union([ + Type.Object({ + x: Type.Number({ default: 1 }), + y: Type.Number({ default: 2 }), + }), + Type.String({ default: 'hello' }), + ]) + const R = Value.Default(T, undefined) + Assert.IsEqual(R, 'hello') + }) + it('Should default interior 3', () => { + const T = Type.Union([ + Type.Object({ + x: Type.Number({ default: 1 }), + y: Type.Number({ default: 2 }), + }), + Type.String({ default: 'hello' }), + ]) + const R = Value.Default(T, 'world') + Assert.IsEqual(R, 'world') + }) + it('Should default interior 4', () => { + const T = Type.Union([ + Type.Object({ + x: Type.Number({ default: 1 }), + y: Type.Number({ default: 2 }), + }), + Type.String({ default: 'hello' }), + ]) + const R = Value.Default(T, {}) + Assert.IsEqual(R, { x: 1, y: 2 }) + }) + it('Should default interior 5', () => { + const T = Type.Union([ + Type.Object({ + x: Type.Number({ default: 1 }), + y: Type.Number({ default: 2 }), + }), + Type.String({ default: 'hello' }), + ]) + const R = Value.Default(T, { x: 3 }) + Assert.IsEqual(R, { x: 3, y: 2 }) + }) + it('Should default interior 6', () => { + const T = Type.Union([ + Type.Object({ + x: Type.Number({ default: 1 }), + y: Type.Number({ default: 2 }), + }), + Type.String({ default: 'hello' }), + ]) + const R = Value.Default(T, { x: 3, y: 4 }) + Assert.IsEqual(R, { x: 3, y: 4 }) + }) + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/993 + // ---------------------------------------------------------------- + it('Should return the original value if no schemas match (cloned interior variant)', async () => { + const T = Type.Union([ + Type.Tuple([Type.Number(), Type.Number()]), + Type.Array(Type.Number()) + ]) + const value = ['hello'] + const R = Value.Default(T, value) + Assert.IsTrue(R === value) + }) + // ---------------------------------------------------------------- + // Interior Unsafe + // ---------------------------------------------------------------- + it('Should default interior unsafe 1', () => { + TypeRegistry.Set('DefaultUnsafe', (schema, value) => typeof value === 'string') + const T = Type.Union([ + Type.Object({ + x: Type.Number({ default: 1 }), + y: Type.Number({ default: 2 }), + }), + Type.Unsafe({ [Kind]: 'DefaultUnsafe', default: 'hello' }), + ]) + const R = Value.Default(T, undefined) + Assert.IsEqual(R, 'hello') + TypeRegistry.Delete('DefaultUnsafe') + }) + it('Should default interior unsafe 2', () => { + TypeRegistry.Set('DefaultUnsafe', (schema, value) => typeof value === 'string') + const T = Type.Union([ + Type.Object({ + x: Type.Number({ default: 1 }), + y: Type.Number({ default: 2 }), + }), + Type.Unsafe({ [Kind]: 'DefaultUnsafe', default: 'hello' }), + ]) + const R = Value.Default(T, 'world') + Assert.IsEqual(R, 'world') + TypeRegistry.Delete('DefaultUnsafe') + }) +}) diff --git a/test/runtime/value/default/unknown.ts b/test/runtime/value/default/unknown.ts new file mode 100644 index 000000000..97236570d --- /dev/null +++ b/test/runtime/value/default/unknown.ts @@ -0,0 +1,16 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/default/Unknown', () => { + it('Should use default', () => { + const T = Type.Unknown({ default: 1 }) + const R = Value.Default(T, undefined) + Assert.IsEqual(R, 1) + }) + it('Should use value', () => { + const T = Type.Unknown({ default: 1 }) + const R = Value.Default(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/default/void.ts b/test/runtime/value/default/void.ts new file mode 100644 index 000000000..03d03c1e3 --- /dev/null +++ b/test/runtime/value/default/void.ts @@ -0,0 +1,16 @@ +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('value/default/Void', () => { + it('Should use default', () => { + const T = Type.Void({ default: 1 }) + const R = Value.Default(T, undefined) + Assert.IsEqual(R, 1) + }) + it('Should use value', () => { + const T = Type.Void({ default: 1 }) + const R = Value.Default(T, null) + Assert.IsEqual(R, null) + }) +}) diff --git a/test/runtime/value/delta/diff.ts b/test/runtime/value/delta/diff.ts new file mode 100644 index 000000000..6be662fcb --- /dev/null +++ b/test/runtime/value/delta/diff.ts @@ -0,0 +1,395 @@ +import { Value, Edit } from '@sinclair/typebox/value' +import { Assert } from '../../assert/index' + +// ----------------------------------------------------------------- +// Diff Factory +// ----------------------------------------------------------------- +function Update(path: string, value: unknown): Edit { + return { type: 'update', path, value } as any +} +function Insert(path: string, value: unknown): Edit { + return { type: 'insert', path, value } as any +} +function Delete(path: string): Edit { + return { type: 'delete', path } as any +} +describe('value/delta/Diff', () => { + // ---------------------------------------------------- + // Null + // ---------------------------------------------------- + it('Should diff NULL null to null', () => { + const A = null + const B = null + const D = Value.Diff(A, B) + const E = [] as Edit[] + Assert.IsEqual(D, E) + }) + it('Should diff NULL undefined to undefined', () => { + const A = undefined + const B = undefined + const D = Value.Diff(A, B) + const E = [] as Edit[] + Assert.IsEqual(D, E) + }) + it('Should diff NULL string to string', () => { + const A = 'hello' + const B = 'hello' + const D = Value.Diff(A, B) + const E = [] as Edit[] + Assert.IsEqual(D, E) + }) + it('Should diff NULL number to number', () => { + const A = 1 + const B = 1 + const D = Value.Diff(A, B) + const E = [] as Edit[] + Assert.IsEqual(D, E) + }) + it('Should diff NULL boolean to boolean', () => { + const A = true + const B = true + const D = Value.Diff(A, B) + const E = [] as Edit[] + Assert.IsEqual(D, E) + }) + it('Should diff NULL symbol to symbol', () => { + const S = Symbol('A') + const A = S + const B = S + const D = Value.Diff(A, B) + const E = [] as Edit[] + Assert.IsEqual(D, E) + }) + it('Should diff NULL object to object', () => { + const A = { x: 1, y: 2, z: 3 } + const B = { x: 1, y: 2, z: 3 } + const D = Value.Diff(A, B) + const E = [] as Edit[] + Assert.IsEqual(D, E) + }) + it('Should diff NULL array to array', () => { + const A = [1, 2, 3] + const B = [1, 2, 3] + const D = Value.Diff(A, B) + const E = [] as Edit[] + Assert.IsEqual(D, E) + }) + // ---------------------------------------------------- + // Type Change Root + // ---------------------------------------------------- + it('Should diff TYPE change number to null', () => { + const A = 1 + const B = null + const D = Value.Diff(A, B) + const E = [Update('', B)] + Assert.IsEqual(D, E) + }) + it('Should diff TYPE change null to undefined', () => { + const A = null + const B = undefined + const D = Value.Diff(A, B) + const E = [Update('', B)] + Assert.IsEqual(D, E) + }) + it('Should diff TYPE change null to number', () => { + const A = null + const B = 1 + const D = Value.Diff(A, B) + const E = [Update('', B)] + Assert.IsEqual(D, E) + }) + it('Should diff TYPE change null to boolean', () => { + const A = null + const B = true + const D = Value.Diff(A, B) + const E = [Update('', B)] + Assert.IsEqual(D, E) + }) + it('Should diff TYPE change null to string', () => { + const A = null + const B = 'hello' + const D = Value.Diff(A, B) + const E = [Update('', B)] + Assert.IsEqual(D, E) + }) + it('Should diff TYPE change null to symbol', () => { + const A = null + const B = Symbol('A') + const D = Value.Diff(A, B) + const E = [Update('', B)] + Assert.IsEqual(D, E) + }) + it('Should diff TYPE change null to object', () => { + const A = null + const B = { x: 1, y: 1, z: 1 } + const D = Value.Diff(A, B) + const E = [Update('', B)] + Assert.IsEqual(D, E) + }) + it('Should diff TYPE change null to array', () => { + const A = null + const B = [1, 2, 3] + const D = Value.Diff(A, B) + const E = [Update('', B)] + Assert.IsEqual(D, E) + }) + // ---------------------------------------------------- + // Value Change Root + // ---------------------------------------------------- + it('Should diff VALUE change number', () => { + const A = 1 + const B = 2 + const D = Value.Diff(A, B) + const E = [Update('', B)] + Assert.IsEqual(D, E) + }) + it('Should diff VALUE change boolean', () => { + const A = false + const B = true + const D = Value.Diff(A, B) + const E = [Update('', B)] + Assert.IsEqual(D, E) + }) + it('Should diff VALUE change string', () => { + const A = 'hello' + const B = 'world' + const D = Value.Diff(A, B) + const E = [Update('', B)] + Assert.IsEqual(D, E) + }) + it('Should diff VALUE change symbol', () => { + const A = Symbol('A') + const B = Symbol('B') + const D = Value.Diff(A, B) + const E = [Update('', B)] + Assert.IsEqual(D, E) + }) + // ---------------------------------------------------- + // Array + // ---------------------------------------------------- + it('Should diff ELEMENT update', () => { + const A = [1, 2, 3, 4] + const B = [1, 2, 3, 9] + const D = Value.Diff(A, B) + const E = [Update('/3', 9)] + Assert.IsEqual(D, E) + }) + it('Should diff ELEMENT push', () => { + const A = [1, 2, 3, 4] + const B = [1, 2, 3, 4, 5] + const D = Value.Diff(A, B) + const E = [Insert('/4', 5)] + Assert.IsEqual(D, E) + }) + it('Should diff ELEMENT push twice', () => { + const A = [1, 2, 3, 4] + const B = [1, 2, 3, 4, 5, 6] + const D = Value.Diff(A, B) + const E = [Insert('/4', 5), Insert('/5', 6)] + Assert.IsEqual(D, E) + }) + it('Should diff ELEMENT pop', () => { + const A = [1, 2, 3, 4] + const B = [1, 2, 3] + const D = Value.Diff(A, B) + const E = [Delete('/3')] + Assert.IsEqual(D, E) + }) + it('Should diff ELEMENT pop twice', () => { + const A = [1, 2, 3, 4] + const B = [1, 2] + const D = Value.Diff(A, B) + const E = [Delete('/3'), Delete('/2')] + Assert.IsEqual(D, E) + }) + it('Should diff ELEMENT unshift', () => { + const A = [1, 2, 3, 4] + const B = [2, 3, 4] + const D = Value.Diff(A, B) + const E = [Update('/0', 2), Update('/1', 3), Update('/2', 4), Delete('/3')] + Assert.IsEqual(D, E) + }) + it('Should diff ELEMENT unshift twice', () => { + const A = [1, 2, 3, 4] + const B = [3, 4] + const D = Value.Diff(A, B) + const E = [Update('/0', 3), Update('/1', 4), Delete('/3'), Delete('/2')] + Assert.IsEqual(D, E) + }) + // ---------------------------------------------------- + // Object + // ---------------------------------------------------- + it('Should diff PROPERTY insert', () => { + const A = { x: 1, y: 1 } + const B = { x: 1, y: 1, z: 1 } + const D = Value.Diff(A, B) + const E = [Insert('/z', 1)] + Assert.IsEqual(D, E) + }) + it('Should diff PROPERTY delete', () => { + const A = { x: 1, y: 1, z: 1 } + const B = { x: 1, y: 1 } + const D = Value.Diff(A, B) + const E = [Delete('/z')] + Assert.IsEqual(D, E) + }) + it('Should diff PROPERTY update', () => { + const A = { x: 1, y: 1, z: 1 } + const B = { x: 1, y: 1, z: 2 } + const D = Value.Diff(A, B) + const E = [Update('/z', 2)] + Assert.IsEqual(D, E) + }) + it('Should diff PROPERTY all values', () => { + const A = { x: 1, y: 1, z: 1 } + const B = { x: 2, y: 2, z: 2 } + const D = Value.Diff(A, B) + const E = [Update('/x', 2), Update('/y', 2), Update('/z', 2)] + Assert.IsEqual(D, E) + }) + it('Should diff PROPERTY all delete, all insert', () => { + const A = { x: 1, y: 1, z: 1 } + const B = { a: 2, b: 2, c: 2 } + const D = Value.Diff(A, B) + const E = [Insert('/a', 2), Insert('/b', 2), Insert('/c', 2), Delete('/x'), Delete('/y'), Delete('/z')] + Assert.IsEqual(D, E) + }) + it('Should diff PROPERTY insert, update, and delete order preserved', () => { + const A = { x: 1, y: 1, z: 1, w: 1 } + const B = { a: 2, b: 2, c: 2, w: 2 } + const D = Value.Diff(A, B) + const E = [Insert('/a', 2), Insert('/b', 2), Insert('/c', 2), Update('/w', 2), Delete('/x'), Delete('/y'), Delete('/z')] + Assert.IsEqual(D, E) + }) + // ---------------------------------------------------- + // Object Nested + // ---------------------------------------------------- + it('Should diff NESTED OBJECT diff type change update', () => { + const A = { v: 1 } + const B = { v: { x: 1, y: 1, z: 1 } } + const D = Value.Diff(A, B) + const E = [Update('/v', B.v)] + Assert.IsEqual(D, E) + }) + it('Should diff NESTED OBJECT diff value change update', () => { + const A = { v: 1 } + const B = { v: 2 } + const D = Value.Diff(A, B) + const E = [Update('/v', B.v)] + Assert.IsEqual(D, E) + }) + it('Should diff NESTED OBJECT diff partial property update', () => { + const A = { v: { x: 1, y: 1, z: 1 } } + const B = { v: { x: 2, y: 2, z: 2 } } + const D = Value.Diff(A, B) + const E = [Update('/v/x', B.v.x), Update('/v/y', B.v.y), Update('/v/z', B.v.z)] + Assert.IsEqual(D, E) + }) + it('Should diff NESTED OBJECT diff partial property insert', () => { + const A = { v: { x: 1, y: 1, z: 1 } } + const B = { v: { x: 1, y: 1, z: 1, w: 1 } } + const D = Value.Diff(A, B) + const E = [Insert('/v/w', B.v.w)] + Assert.IsEqual(D, E) + }) + it('Should diff NESTED OBJECT diff partial property delete', () => { + const A = { v: { x: 1, y: 1, z: 1 } } + const B = { v: { x: 1, y: 1 } } + const D = Value.Diff(A, B) + const E = [Delete('/v/z')] + Assert.IsEqual(D, E) + }) + it('Should diff NESTED OBJECT ordered diff - update, insert and delete', () => { + const A = { v: { x: 1, y: 1 } } + const B = { v: { x: 2, w: 2 } } + const D = Value.Diff(A, B) + const E = [Insert('/v/w', B.v.w), Update('/v/x', B.v.x), Delete('/v/y')] + Assert.IsEqual(D, E) + }) + // ---------------------------------------------------- + // Array Nested + // ---------------------------------------------------- + it('Should diff NESTED ARRAY object diff type change update', () => { + const A = [{ v: 1 }] + const B = [{ v: { x: 1, y: 1, z: 1 } }] + const D = Value.Diff(A, B) + const E = [Update('/0/v', B[0].v)] + Assert.IsEqual(D, E) + }) + it('Should diff NESTED ARRAY object diff value change update', () => { + const A = [{ v: 1 }] + const B = [{ v: 2 }] + const D = Value.Diff(A, B) + const E = [Update('/0/v', B[0].v)] + Assert.IsEqual(D, E) + }) + it('Should diff NESTED ARRAY object diff partial property update', () => { + const A = [{ v: { x: 1, y: 1, z: 1 } }] + const B = [{ v: { x: 2, y: 2, z: 2 } }] + const D = Value.Diff(A, B) + const E = [Update('/0/v/x', B[0].v.x), Update('/0/v/y', B[0].v.y), Update('/0/v/z', B[0].v.z)] + Assert.IsEqual(D, E) + }) + it('Should diff NESTED ARRAY object diff partial property insert', () => { + const A = [{ v: { x: 1, y: 1, z: 1 } }] + const B = [{ v: { x: 1, y: 1, z: 1, w: 1 } }] + const D = Value.Diff(A, B) + const E = [Insert('/0/v/w', B[0].v.w)] + Assert.IsEqual(D, E) + }) + it('Should diff NESTED ARRAY object diff partial property delete', () => { + const A = [{ v: { x: 1, y: 1, z: 1 } }] + const B = [{ v: { x: 1, y: 1 } }] + const D = Value.Diff(A, B) + const E = [Delete('/0/v/z')] + Assert.IsEqual(D, E) + }) + it('Should diff NESTED ARRAY insert update and delete order preserved', () => { + const A = [{ v: { x: 1, y: 1 } }] + const B = [{ v: { x: 2, w: 2 } }] + const D = Value.Diff(A, B) + const E = [Insert('/0/v/w', B[0].v.w), Update('/0/v/x', B[0].v.x), Delete('/0/v/y')] + Assert.IsEqual(D, E) + }) + it('Should throw if attempting to diff a current value with symbol key', () => { + const A = [{ [Symbol('A')]: 1, v: { x: 1, y: 1 } }] + const B = [{ v: { x: 2, w: 2 } }] + Assert.Throws(() => Value.Diff(A, B)) + }) + it('Should throw if attempting to diff a next value with symbol key', () => { + const A = [{ v: { x: 1, y: 1 } }] + const B = [{ [Symbol('A')]: 1, v: { x: 2, w: 2 } }] + Assert.Throws(() => Value.Diff(A, B)) + }) + it('Should diff a Uint8Array (same size)', () => { + const A = new Uint8Array([0, 1, 2, 3]) + const B = new Uint8Array([0, 9, 2, 9]) + const D = Value.Diff(A, B) + const E = [Update('/1', 9), Update('/3', 9)] + Assert.IsEqual(D, E) + }) + it('Should diff a Uint8Array (less than requires full update)', () => { + const A = new Uint8Array([0, 1, 2, 3]) + const B = new Uint8Array([0, 9, 2]) + const D = Value.Diff(A, B) + const E = [Update('', new Uint8Array([0, 9, 2]))] + Assert.IsEqual(D, E) + }) + it('Should diff a Uint8Array (greater than requires full update)', () => { + const A = new Uint8Array([0, 1, 2, 3]) + const B = new Uint8Array([0, 9, 2, 3, 4]) + const D = Value.Diff(A, B) + const E = [Update('', new Uint8Array([0, 9, 2, 3, 4]))] + Assert.IsEqual(D, E) + }) + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/937 + // ---------------------------------------------------------------- + it('Should generate no diff for undefined properties of current and next', () => { + const A = { a: undefined } + const B = { a: undefined } + const D = Value.Diff(A, B) + const E = [] as any + Assert.IsEqual(D, E) + }) +}) diff --git a/test/runtime/value/delta/index.ts b/test/runtime/value/delta/index.ts new file mode 100644 index 000000000..10b4755d9 --- /dev/null +++ b/test/runtime/value/delta/index.ts @@ -0,0 +1,2 @@ +import './diff' +import './patch' diff --git a/test/runtime/value/delta/patch.ts b/test/runtime/value/delta/patch.ts new file mode 100644 index 000000000..2addfe53c --- /dev/null +++ b/test/runtime/value/delta/patch.ts @@ -0,0 +1,424 @@ +import { Value } from '@sinclair/typebox/value' +import { Assert } from '../../assert/index' + +describe('value/delta/Patch', () => { + // ---------------------------------------------------- + // Null + // ---------------------------------------------------- + it('Should patch NULL null to null', () => { + const A = null + const B = null + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + it('Should patch NULL undefined to undefined', () => { + const A = undefined + const B = undefined + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + it('Should patch NULL string to string', () => { + const A = 'hello' + const B = 'hello' + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + it('Should patch NULL number to number', () => { + const A = 1 + const B = 1 + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + it('Should patch NULL boolean to boolean', () => { + const A = true + const B = true + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + it('Should patch NULL symbol to symbol', () => { + const S = Symbol('A') + const A = S + const B = S + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + it('Should patch NULL object to object', () => { + const A = { x: 1, y: 2, z: 3 } + const B = { x: 1, y: 2, z: 3 } + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + it('Should patch NULL array to array', () => { + const A = [1, 2, 3] + const B = [1, 2, 3] + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + // ---------------------------------------------------- + // Type Change Root + // ---------------------------------------------------- + it('Should patch TYPE change number to null', () => { + const A = 1 + const B = null + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + it('Should patch TYPE change null to undefined', () => { + const A = null + const B = undefined + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + it('Should patch TYPE change null to number', () => { + const A = null + const B = 1 + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + it('Should patch TYPE change null to boolean', () => { + const A = null + const B = true + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + it('Should patch TYPE change null to string', () => { + const A = null + const B = 'hello' + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + it('Should patch TYPE change null to symbol', () => { + const A = null + const B = Symbol('A') + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + it('Should patch TYPE change null to object', () => { + const A = null + const B = { x: 1, y: 1, z: 1 } + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + it('Should patch TYPE change null to array', () => { + const A = null + const B = [1, 2, 3] + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + it('Should patch TYPE change object to array', () => { + const A = { x: 1, y: 2 } + const B = [1, 2, 3] + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + it('Should patch TYPE change array to object', () => { + const A = [1, 2, 3] + const B = { x: 1, y: 2 } + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + // ---------------------------------------------------- + // Value Change Root + // ---------------------------------------------------- + it('Should patch VALUE change number', () => { + const A = 1 + const B = 2 + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + it('Should patch VALUE change boolean', () => { + const A = false + const B = true + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + it('Should patch VALUE change string', () => { + const A = 'hello' + const B = 'world' + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + it('Should patch VALUE change symbol', () => { + const A = Symbol('A') + const B = Symbol('B') + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + // ---------------------------------------------------- + // Array + // ---------------------------------------------------- + it('Should patch ELEMENT update', () => { + const A = [1, 2, 3, 4] + const B = [1, 2, 3, 9] + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + it('Should patch ELEMENT push', () => { + const A = [1, 2, 3, 4] + const B = [1, 2, 3, 4, 5] + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + it('Should patch ELEMENT push twice', () => { + const A = [1, 2, 3, 4] + const B = [1, 2, 3, 4, 5, 6] + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + it('Should patch ELEMENT pop', () => { + const A = [1, 2, 3, 4] + const B = [1, 2, 3] + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + it('Should patch ELEMENT pop twice', () => { + const A = [1, 2, 3, 4] + const B = [1, 2] + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + it('Should patch ELEMENT unshift', () => { + const A = [1, 2, 3, 4] + const B = [2, 3, 4] + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + it('Should patch ELEMENT unshift twice', () => { + const A = [1, 2, 3, 4] + const B = [3, 4] + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + // ---------------------------------------------------- + // Object + // ---------------------------------------------------- + it('Should patch PROPERTY insert', () => { + const A = { x: 1, y: 1 } + const B = { x: 1, y: 1, z: 1 } + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + it('Should patch PROPERTY delete', () => { + const A = { x: 1, y: 1, z: 1 } + const B = { x: 1, y: 1 } + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + it('Should patch PROPERTY update', () => { + const A = { x: 1, y: 1, z: 1 } + const B = { x: 1, y: 1, z: 2 } + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + it('Should patch PROPERTY all values', () => { + const A = { x: 1, y: 1, z: 1 } + const B = { x: 2, y: 2, z: 2 } + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + it('Should patch PROPERTY all delete, all insert', () => { + const A = { x: 1, y: 1, z: 1 } + const B = { a: 2, b: 2, c: 2 } + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + it('Should patch PROPERTY update, insert and delete order preserved', () => { + const A = { x: 1, y: 1, z: 1, w: 1 } + const B = { a: 2, b: 2, c: 2, w: 2 } + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + // ---------------------------------------------------- + // Object Nested + // ---------------------------------------------------- + it('Should patch NESTED OBJECT diff type change update', () => { + const A = { v: 1 } + const B = { v: { x: 1, y: 1, z: 1 } } + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + it('Should patch NESTED OBJECT diff value change update', () => { + const A = { v: 1 } + const B = { v: 2 } + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + it('Should patch NESTED OBJECT diff partial property update', () => { + const A = { v: { x: 1, y: 1, z: 1 } } + const B = { v: { x: 2, y: 2, z: 2 } } + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + it('Should patch NESTED OBJECT update, insert and delete order preserved', () => { + const A = { v: { x: 1, y: 1 } } + const B = { v: { x: 2, w: 2 } } + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + // ---------------------------------------------------- + // Array Nested + // ---------------------------------------------------- + it('Should patch NESTED ARRAY object diff type change update', () => { + const A = [{ v: 1 }] + const B = [{ v: { x: 1, y: 1, z: 1 } }] + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + it('Should patch NESTED ARRAY object diff value change update', () => { + const A = [{ v: 1 }] + const B = [{ v: 2 }] + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + it('Should patch NESTED ARRAY object diff partial property update', () => { + const A = [{ v: { x: 1, y: 1, z: 1 } }] + const B = [{ v: { x: 2, y: 2, z: 2 } }] + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + it('Should patch NESTED ARRAY object diff partial property insert', () => { + const A = [{ v: { x: 1, y: 1, z: 1 } }] + const B = [{ v: { x: 1, y: 1, z: 1, w: 1 } }] + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + it('Should patch NESTED ARRAY object diff partial property delete', () => { + const A = [{ v: { x: 1, y: 1, z: 1 } }] + const B = [{ v: { x: 1, y: 1 } }] + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + it('Should patch NESTED ARRAY object ordered diff - update, insert and delete', () => { + const A = [{ v: { x: 1, y: 1 } }] + const B = [{ v: { x: 2, w: 2 } }] + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + it('Should patch Uint8Array (same size)', () => { + const A = [{ v: new Uint8Array([0, 1, 3]) }] + const B = [{ v: new Uint8Array([0, 1, 2]) }] + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + it('Should patch Uint8Array (less than size)', () => { + const A = [{ v: new Uint8Array([0, 1, 3]) }] + const B = [{ v: new Uint8Array([0, 1]) }] + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + it('Should patch Uint8Array (greater than size)', () => { + const A = [{ v: new Uint8Array([0, 1, 3]) }] + const B = [{ v: new Uint8Array([0, 1, 2, 4]) }] + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + // ---------------------------------------------------- + // Mega Values + // ---------------------------------------------------- + it('Should patch MEGA value', () => { + const A = [ + { a: { x: 1, y: 1 } }, + { b: { x: 1, y: 1, z: ['hello', 1, 2] } }, + { c: { x: 1, y: 1 } }, + { + d: [ + { a: 1 }, + { a: 1 }, + { a: 1 }, + [ + { a: { x: 1, y: 1 } }, + { b: { x: 1, y: 1, z: ['hello', true, true, 2] } }, + { c: { x: 1, y: 1 } }, + { + d: [{ a: 1 }, { a: 1 }, { a: 1 }], + }, + ], + ], + }, + ] + const B = [ + 1, + 2, + { a: { x: 1, y: 'a' } }, + { b: { x: true, y: 1, z: ['hello', true, true] } }, + { c: { x: 1, y: 1 } }, + { + d: [ + { a: 'hello' }, + 1, + 2, + { a: 1 }, + { a: 1 }, + [ + { a: { x: 1, y: 1 }, x: [1, 2, 3, 4] }, + { c: { x: 1, y: 1 } }, + { + d: [{ a: 1 }, { a: 2 }, 'hello'], + }, + ], + ], + }, + ] + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/937 + // ---------------------------------------------------------------- + it('Should generate no diff for undefined properties of current and next', () => { + const A = { a: undefined } + const B = { a: undefined } + const D = Value.Diff(A, B) + const P = Value.Patch(A, D) + Assert.IsEqual(B, P) + }) +}) diff --git a/test/runtime/value/equal/equal.ts b/test/runtime/value/equal/equal.ts new file mode 100644 index 000000000..57382d800 --- /dev/null +++ b/test/runtime/value/equal/equal.ts @@ -0,0 +1,93 @@ +import { Value } from '@sinclair/typebox/value' +import { Assert } from '../../assert/index' + +describe('value/equal/Equal', () => { + it('Should equal null value', () => { + const R = Value.Equal(null, null) + Assert.IsTrue(R) + }) + it('Should not equal null value', () => { + const R = Value.Equal(null, 'null') + Assert.IsFalse(R) + }) + it('Should equal undefined value', () => { + const R = Value.Equal(undefined, undefined) + Assert.IsTrue(R) + }) + it('Should not equal undefined value', () => { + const R = Value.Equal(undefined, 'undefined') + Assert.IsFalse(R) + }) + it('Should equal symbol value', () => { + const S1 = Symbol.for('test') + const S2 = Symbol.for('test') + const R = Value.Equal(S1, S2) + Assert.IsTrue(R) + }) + it('Should not equal symbol value', () => { + const S1 = Symbol('test') + const S2 = Symbol('test') + const R = Value.Equal(S1, S2) + Assert.IsFalse(R) + }) + it('Should equal string value', () => { + const R = Value.Equal('hello', 'hello') + Assert.IsTrue(R) + }) + it('Should not equal string value', () => { + const R = Value.Equal('hello', 'world') + Assert.IsFalse(R) + }) + it('Should equal number value', () => { + const R = Value.Equal(1, 1) + Assert.IsTrue(R) + }) + it('Should not equal number value', () => { + const R = Value.Equal(1, 2) + Assert.IsFalse(R) + }) + it('Should equal boolean value', () => { + const R = Value.Equal(true, true) + Assert.IsTrue(R) + }) + it('Should not equal boolean value', () => { + const R = Value.Equal(true, false) + Assert.IsFalse(R) + }) + it('Should equal array value', () => { + const R = Value.Equal([0, 1, 2], [0, 1, 2]) + Assert.IsTrue(R) + }) + it('Should not equal array value', () => { + const R = Value.Equal([0, 1, 2], [0, 1, 3]) + Assert.IsFalse(R) + }) + it('Should not equal array value with additional elements', () => { + const R = Value.Equal([0, 1, 2], [0, 1, 2, 3]) + Assert.IsFalse(R) + }) + it('Should equal object value', () => { + const R = Value.Equal({ x: 1, y: 2, z: 3 }, { x: 1, y: 2, z: 3 }) + Assert.IsTrue(R) + }) + it('Should not equal object value', () => { + const R = Value.Equal({ x: 1, y: 2, z: 3 }, { x: 1, y: 2, z: 4 }) + Assert.IsFalse(R) + }) + it('Should not equal object value with additional properties', () => { + const R = Value.Equal({ x: 1, y: 2, z: 3 }, { x: 1, y: 2, z: 3, w: 1 }) + Assert.IsFalse(R) + }) + it('Should equal typed array value', () => { + const R = Value.Equal(new Uint8Array([0, 1, 2]), new Uint8Array([0, 1, 2])) + Assert.IsTrue(R) + }) + it('Should not equal typed array value', () => { + const R = Value.Equal(new Uint8Array([0, 1, 2]), new Uint8Array([0, 1, 3])) + Assert.IsFalse(R) + }) + it('Should not equal typed array value with varying type', () => { + const R = Value.Equal(new Uint8Array([0, 1, 2]), new Int8Array([0, 1, 2])) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/value/equal/index.ts b/test/runtime/value/equal/index.ts new file mode 100644 index 000000000..8175d94d7 --- /dev/null +++ b/test/runtime/value/equal/index.ts @@ -0,0 +1 @@ +import './equal' diff --git a/test/runtime/value/guard/guard.ts b/test/runtime/value/guard/guard.ts new file mode 100644 index 000000000..88aaec6f2 --- /dev/null +++ b/test/runtime/value/guard/guard.ts @@ -0,0 +1,511 @@ +import { Assert } from '../../assert/index' +import * as ValueGuard from '@sinclair/typebox/value' + +describe('value/guard/ValueGuard', () => { + // ----------------------------------------------------- + // IsNull + // ----------------------------------------------------- + it('Should guard Null 1', () => { + const R = ValueGuard.IsNull(null) + Assert.IsTrue(R) + }) + it('Should guard Null 2', () => { + const R = ValueGuard.IsNull({}) + Assert.IsFalse(R) + }) + // ----------------------------------------------------- + // IsUndefined + // ----------------------------------------------------- + it('Should guard Undefined 1', () => { + const R = ValueGuard.IsUndefined(undefined) + Assert.IsTrue(R) + }) + it('Should guard Undefined 2', () => { + const R = ValueGuard.IsUndefined({}) + Assert.IsFalse(R) + }) + // ----------------------------------------------------- + // IsBigInt + // ----------------------------------------------------- + it('Should guard BigInt 1', () => { + const R = ValueGuard.IsBigInt(1n) + Assert.IsTrue(R) + }) + it('Should guard BigInt 2', () => { + const R = ValueGuard.IsBigInt(1) + Assert.IsFalse(R) + }) + it('Should guard BigInt 3', () => { + const R = ValueGuard.IsBigInt('123') + Assert.IsFalse(R) + }) + // ----------------------------------------------------- + // IsNumber + // ----------------------------------------------------- + it('Should guard Number 1', () => { + const R = ValueGuard.IsNumber(1) + Assert.IsTrue(R) + }) + it('Should guard Number 2', () => { + const R = ValueGuard.IsNumber(3.14) + Assert.IsTrue(R) + }) + it('Should guard Number 3', () => { + const R = ValueGuard.IsNumber('') + Assert.IsFalse(R) + }) + it('Should guard Number 4', () => { + const R = ValueGuard.IsNumber(NaN) + Assert.IsTrue(R) + }) + // ----------------------------------------------------- + // IsString + // ----------------------------------------------------- + it('Should guard String 1', () => { + const R = ValueGuard.IsString('') + Assert.IsTrue(R) + }) + it('Should guard String 2', () => { + const R = ValueGuard.IsString(true) + Assert.IsFalse(R) + }) + // ----------------------------------------------------- + // IsBoolean + // ----------------------------------------------------- + it('Should guard Boolean 1', () => { + const R = ValueGuard.IsBoolean(true) + Assert.IsTrue(R) + }) + it('Should guard Boolean 2', () => { + const R = ValueGuard.IsBoolean(1) + Assert.IsFalse(R) + }) + // ----------------------------------------------------- + // IsObject + // ----------------------------------------------------- + it('Should guard Object 1', () => { + const R = ValueGuard.IsObject({}) + Assert.IsTrue(R) + }) + it('Should guard Object 2', () => { + const R = ValueGuard.IsObject(1) + Assert.IsFalse(R) + }) + it('Should guard Object 3', () => { + const R = ValueGuard.IsObject([]) + Assert.IsTrue(R) + }) + // ----------------------------------------------------- + // IsArray + // ----------------------------------------------------- + it('Should guard Array 1', () => { + const R = ValueGuard.IsArray([]) + Assert.IsTrue(R) + }) + it('Should guard Array 2', () => { + const R = ValueGuard.IsArray({}) + Assert.IsFalse(R) + }) + // ----------------------------------------------------- + // IsAsyncIterator + // ----------------------------------------------------- + it('Should guard AsyncIterator 1', () => { + const R = ValueGuard.IsAsyncIterator((async function* (): any {})()) + Assert.IsTrue(R) + }) + it('Should guard AsyncIterator 2', () => { + const R = ValueGuard.IsAsyncIterator((function* (): any {})()) + Assert.IsFalse(R) + }) + // ----------------------------------------------------- + // HasPropertyKey + // ----------------------------------------------------- + it('Should guard property key 1', () => { + const O = { x: 10 } + const R = ValueGuard.HasPropertyKey(O, 'x') + Assert.IsTrue(R) + }) + it('Should guard property key 2', () => { + const O = { x: 10 } + const R = ValueGuard.HasPropertyKey(O, 'y') + Assert.IsFalse(R) + }) + // ----------------------------------------------------- + // IsDate + // ----------------------------------------------------- + it('Should guard Date 1', () => { + const R = ValueGuard.IsDate(new Date()) + Assert.IsTrue(R) + }) + it('Should guard Date 2', () => { + const R = ValueGuard.IsDate({}) + Assert.IsFalse(R) + }) + // ----------------------------------------------------- + // IsFunction + // ----------------------------------------------------- + it('Should guard Function 1', () => { + const R = ValueGuard.IsFunction(function () {}) + Assert.IsTrue(R) + }) + it('Should guard Function 2', () => { + const R = ValueGuard.IsFunction({}) + Assert.IsFalse(R) + }) + // ----------------------------------------------------- + // IsInteger + // ----------------------------------------------------- + it('Should guard Integer 1', () => { + const R = ValueGuard.IsInteger(1) + Assert.IsTrue(R) + }) + it('Should guard Integer 2', () => { + const R = ValueGuard.IsInteger(3.14) + Assert.IsFalse(R) + }) + it('Should guard Integer 3', () => { + const R = ValueGuard.IsInteger(NaN) + Assert.IsFalse(R) + }) + // ----------------------------------------------------- + // IsIterator + // ----------------------------------------------------- + it('Should guard Iterator 1', () => { + const R = ValueGuard.IsIterator((function* () {})()) + Assert.IsTrue(R) + }) + it('Should guard Iterator 2', () => { + const R = ValueGuard.IsIterator({}) + Assert.IsFalse(R) + }) + // ----------------------------------------------------- + // IsStandardObject + // ----------------------------------------------------- + it('Should guard StandardObject 1', () => { + const R = ValueGuard.IsStandardObject({}) + Assert.IsTrue(R) + }) + it('Should guard StandardObject 2', () => { + const R = ValueGuard.IsStandardObject(1) + Assert.IsFalse(R) + }) + it('Should guard StandardObject 3', () => { + const R = ValueGuard.IsStandardObject([]) + Assert.IsFalse(R) + }) + it('Should guard StandardObject 4', () => { + const R = ValueGuard.IsStandardObject(new (class {})()) + Assert.IsFalse(R) + }) + it('Should guard StandardObject 5', () => { + const R = ValueGuard.IsStandardObject(Object.create(null)) + Assert.IsTrue(R) + }) + // ----------------------------------------------------- + // IsInstanceObject + // ----------------------------------------------------- + it('Should guard InstanceObject 1', () => { + const R = ValueGuard.IsInstanceObject({}) + Assert.IsFalse(R) + }) + it('Should guard InstanceObject 2', () => { + const R = ValueGuard.IsInstanceObject(1) + Assert.IsFalse(R) + }) + it('Should guard InstanceObject 3', () => { + const R = ValueGuard.IsInstanceObject([]) + Assert.IsFalse(R) + }) + it('Should guard InstanceObject 4', () => { + const R = ValueGuard.IsInstanceObject(new (class {})()) + Assert.IsTrue(R) + }) + // ----------------------------------------------------- + // IsPromise + // ----------------------------------------------------- + it('Should guard Promise 1', () => { + const R = ValueGuard.IsPromise(Promise.resolve(1)) + Assert.IsTrue(R) + }) + it('Should guard Promise 2', () => { + const R = ValueGuard.IsPromise(new (class {})()) + Assert.IsFalse(R) + }) + // ----------------------------------------------------- + // IsSymbol + // ----------------------------------------------------- + it('Should guard Symbol 1', () => { + const R = ValueGuard.IsSymbol(Symbol(1)) + Assert.IsTrue(R) + }) + it('Should guard Symbol 2', () => { + const R = ValueGuard.IsSymbol(1) + Assert.IsFalse(R) + }) + // ----------------------------------------------------- + // IsTypedArray + // ----------------------------------------------------- + it('Should guard TypedArray 1', () => { + const R = ValueGuard.IsTypedArray(new Uint8Array(1)) + Assert.IsTrue(R) + }) + it('Should guard TypedArray 2', () => { + const R = ValueGuard.IsTypedArray(new Float32Array(1)) + Assert.IsTrue(R) + }) + it('Should guard TypedArray 3', () => { + const R = ValueGuard.IsTypedArray(new ArrayBuffer(1)) + Assert.IsFalse(R) + }) + it('Should guard TypedArray 4', () => { + const R = ValueGuard.IsTypedArray(1) + Assert.IsFalse(R) + }) + // ----------------------------------------------------- + // IsInt8Array + // ----------------------------------------------------- + it('Should guard Int8Array 1', () => { + const R = ValueGuard.IsInt8Array(new Int8Array(1)) + Assert.IsTrue(R) + }) + it('Should guard Int8Array 2', () => { + const R = ValueGuard.IsInt8Array(new Float32Array(1)) + Assert.IsFalse(R) + }) + it('Should guard Int8Array 2', () => { + const R = ValueGuard.IsInt8Array(1) + Assert.IsFalse(R) + }) + // ----------------------------------------------------- + // IsUint8Array + // ----------------------------------------------------- + it('Should guard Uint8Array 1', () => { + const R = ValueGuard.IsUint8Array(new Uint8Array(1)) + Assert.IsTrue(R) + }) + it('Should guard Uint8Array 2', () => { + const R = ValueGuard.IsUint8Array(new Float32Array(1)) + Assert.IsFalse(R) + }) + it('Should guard Uint8Array 2', () => { + const R = ValueGuard.IsUint8Array(1) + Assert.IsFalse(R) + }) + // ----------------------------------------------------- + // IsUint8ClampedArray + // ----------------------------------------------------- + it('Should guard Uint8ClampedArray 1', () => { + const R = ValueGuard.IsUint8ClampedArray(new Uint8ClampedArray(1)) + Assert.IsTrue(R) + }) + it('Should guard Uint8ClampedArray 2', () => { + const R = ValueGuard.IsUint8ClampedArray(new Float32Array(1)) + Assert.IsFalse(R) + }) + it('Should guard Uint8ClampedArray 2', () => { + const R = ValueGuard.IsUint8ClampedArray(1) + Assert.IsFalse(R) + }) + + // ----------------------------------------------------- + // IsInt16Array + // ----------------------------------------------------- + it('Should guard Int16Array 1', () => { + const R = ValueGuard.IsInt16Array(new Int16Array(1)) + Assert.IsTrue(R) + }) + it('Should guard Int16Array 2', () => { + const R = ValueGuard.IsInt16Array(new Float32Array(1)) + Assert.IsFalse(R) + }) + it('Should guard Int16Array 2', () => { + const R = ValueGuard.IsInt16Array(1) + Assert.IsFalse(R) + }) + // ----------------------------------------------------- + // IsUint16Array + // ----------------------------------------------------- + it('Should guard Uint16Array 1', () => { + const R = ValueGuard.IsUint16Array(new Uint16Array(1)) + Assert.IsTrue(R) + }) + it('Should guard Uint16Array 2', () => { + const R = ValueGuard.IsUint16Array(new Float32Array(1)) + Assert.IsFalse(R) + }) + it('Should guard Uint16Array 2', () => { + const R = ValueGuard.IsUint16Array(1) + Assert.IsFalse(R) + }) + // ----------------------------------------------------- + // IsInt32Array + // ----------------------------------------------------- + it('Should guard Int32Array 1', () => { + const R = ValueGuard.IsInt32Array(new Int32Array(1)) + Assert.IsTrue(R) + }) + it('Should guard Int32Array 2', () => { + const R = ValueGuard.IsInt32Array(new Float32Array(1)) + Assert.IsFalse(R) + }) + it('Should guard Int32Array 2', () => { + const R = ValueGuard.IsInt32Array(1) + Assert.IsFalse(R) + }) + // ----------------------------------------------------- + // IsUint32Array + // ----------------------------------------------------- + it('Should guard Uint32Array 1', () => { + const R = ValueGuard.IsUint32Array(new Uint32Array(1)) + Assert.IsTrue(R) + }) + it('Should guard Uint32Array 2', () => { + const R = ValueGuard.IsUint32Array(new Float32Array(1)) + Assert.IsFalse(R) + }) + it('Should guard Uint32Array 2', () => { + const R = ValueGuard.IsUint32Array(1) + Assert.IsFalse(R) + }) + // ----------------------------------------------------- + // IsFloat32Array + // ----------------------------------------------------- + it('Should guard Float32Array 1', () => { + const R = ValueGuard.IsFloat32Array(new Float32Array(1)) + Assert.IsTrue(R) + }) + it('Should guard Float32Array 2', () => { + const R = ValueGuard.IsFloat32Array(new Float64Array(1)) + Assert.IsFalse(R) + }) + it('Should guard Float32Array 2', () => { + const R = ValueGuard.IsFloat32Array(1) + Assert.IsFalse(R) + }) + // ----------------------------------------------------- + // IsFloat64Array + // ----------------------------------------------------- + it('Should guard Float64Array 1', () => { + const R = ValueGuard.IsFloat64Array(new Float64Array(1)) + Assert.IsTrue(R) + }) + it('Should guard Float64Array 2', () => { + const R = ValueGuard.IsFloat64Array(new Float32Array(1)) + Assert.IsFalse(R) + }) + it('Should guard Float64Array 2', () => { + const R = ValueGuard.IsFloat64Array(1) + Assert.IsFalse(R) + }) + // ----------------------------------------------------- + // IsBigInt64Array + // ----------------------------------------------------- + it('Should guard BigInt64Array 1', () => { + const R = ValueGuard.IsBigInt64Array(new BigInt64Array(1)) + Assert.IsTrue(R) + }) + it('Should guard BigInt64Array 2', () => { + const R = ValueGuard.IsBigInt64Array(new Float32Array(1)) + Assert.IsFalse(R) + }) + it('Should guard BigInt64Array 2', () => { + const R = ValueGuard.IsBigInt64Array(1) + Assert.IsFalse(R) + }) + // ----------------------------------------------------- + // IsBigUint64Array + // ----------------------------------------------------- + it('Should guard BigUint64Array 1', () => { + const R = ValueGuard.IsBigUint64Array(new BigUint64Array(1)) + Assert.IsTrue(R) + }) + it('Should guard BigUint64Array 2', () => { + const R = ValueGuard.IsBigUint64Array(new Float32Array(1)) + Assert.IsFalse(R) + }) + it('Should guard BigUint64Array 2', () => { + const R = ValueGuard.IsBigUint64Array(1) + Assert.IsFalse(R) + }) + // ----------------------------------------------------- + // IsMap + // ----------------------------------------------------- + it('Should guard Map 1', () => { + const R = ValueGuard.IsMap(new Map()) + Assert.IsTrue(R) + }) + it('Should guard Map 2', () => { + const R = ValueGuard.IsMap(new Float32Array(1)) + Assert.IsFalse(R) + }) + it('Should guard Map 2', () => { + const R = ValueGuard.IsMap(1) + Assert.IsFalse(R) + }) + // ----------------------------------------------------- + // IsSet + // ----------------------------------------------------- + it('Should guard Set 1', () => { + const R = ValueGuard.IsSet(new Set()) + Assert.IsTrue(R) + }) + it('Should guard Set 2', () => { + const R = ValueGuard.IsSet(new Float32Array(1)) + Assert.IsFalse(R) + }) + it('Should guard Set 2', () => { + const R = ValueGuard.IsSet(1) + Assert.IsFalse(R) + }) + // ----------------------------------------------------- + // IsValueType + // ----------------------------------------------------- + it('Should guard value type 1', () => { + const R = ValueGuard.IsValueType(1) + Assert.IsTrue(R) + }) + it('Should guard value type 2', () => { + const R = ValueGuard.IsValueType(true) + Assert.IsTrue(R) + }) + it('Should guard value type 3', () => { + const R = ValueGuard.IsValueType(false) + Assert.IsTrue(R) + }) + it('Should guard value type 4', () => { + const R = ValueGuard.IsValueType('hello') + Assert.IsTrue(R) + }) + it('Should guard value type 5', () => { + const R = ValueGuard.IsValueType(1n) + Assert.IsTrue(R) + }) + it('Should guard value type 6', () => { + const R = ValueGuard.IsValueType(null) + Assert.IsTrue(R) + }) + it('Should guard value type 7', () => { + const R = ValueGuard.IsValueType(undefined) + Assert.IsTrue(R) + }) + it('Should guard value type 8', () => { + const R = ValueGuard.IsValueType(function () {}) + Assert.IsFalse(R) + }) + it('Should guard value type 9', () => { + const R = ValueGuard.IsValueType({}) + Assert.IsFalse(R) + }) + it('Should guard value type 10', () => { + const R = ValueGuard.IsValueType([]) + Assert.IsFalse(R) + }) + it('Should guard value type 11', () => { + const R = ValueGuard.IsValueType(class {}) + Assert.IsFalse(R) + }) + it('Should guard value type 12', () => { + const R = ValueGuard.IsValueType(new (class {})()) + Assert.IsFalse(R) + }) +}) diff --git a/test/runtime/value/guard/index.ts b/test/runtime/value/guard/index.ts new file mode 100644 index 000000000..4f2ca894f --- /dev/null +++ b/test/runtime/value/guard/index.ts @@ -0,0 +1 @@ +import './guard' diff --git a/test/runtime/value/hash/hash.ts b/test/runtime/value/hash/hash.ts new file mode 100644 index 000000000..aceb06480 --- /dev/null +++ b/test/runtime/value/hash/hash.ts @@ -0,0 +1,122 @@ +import { Hash } from '@sinclair/typebox/value' +import { Assert } from '../../assert/index' + +describe('value/hash/Hash', () => { + it('Should hash number', () => { + Assert.IsEqual('bigint', typeof Hash(1)) + const A = Hash(1) + const B = Hash(2) + Assert.NotEqual(A, B) + }) + it('Should hash string', () => { + Assert.IsEqual('bigint', typeof Hash('hello')) + const A = Hash('hello') + const B = Hash('world') + Assert.NotEqual(A, B) + }) + it('Should hash boolean', () => { + Assert.IsEqual('bigint', typeof Hash(true)) + Assert.IsEqual('bigint', typeof Hash(false)) + const A = Hash(true) + const B = Hash(false) + Assert.NotEqual(A, B) + }) + it('Should hash null', () => { + Assert.IsEqual('bigint', typeof Hash(null)) + const A = Hash(null) + const B = Hash(undefined) + Assert.NotEqual(A, B) + }) + it('Should hash array', () => { + Assert.IsEqual('bigint', typeof Hash([0, 1, 2, 3])) + const A = Hash([0, 1, 2, 3]) + const B = Hash([0, 2, 2, 3]) + Assert.NotEqual(A, B) + }) + it('Should hash object 1', () => { + // prettier-ignore + Assert.IsEqual('bigint', typeof Hash({ x: 1, y: 2 })) + const A = Hash({ x: 1, y: 2 }) + const B = Hash({ x: 2, y: 2 }) + Assert.NotEqual(A, B) + }) + it('Should hash object 2', () => { + const A = Hash({ x: 1, y: [1, 2] }) + const B = Hash({ x: 1, y: [1, 3] }) + Assert.NotEqual(A, B) + }) + it('Should hash object 3', () => { + const A = Hash({ x: 1, y: undefined }) + const B = Hash({ x: 1 }) + Assert.NotEqual(A, B) + }) + it('Should hash object 4', () => { + const A = Hash({ x: 1, y: new Uint8Array([0, 1, 2]) }) + const B = Hash({ x: 1, y: [0, 1, 2] }) + Assert.NotEqual(A, B) + }) + it('Should hash object 5', () => { + const A = Hash({ x: 1, y: undefined }) + const B = Hash({ x: 2, y: undefined }) + Assert.NotEqual(A, B) + }) + it('Should hash Date', () => { + Assert.IsEqual('bigint', typeof Hash(new Date())) + const A = Hash(new Date(1)) + const B = Hash(new Date(2)) + Assert.NotEqual(A, B) + }) + it('Should hash Uint8Array', () => { + Assert.IsEqual('bigint', typeof Hash(new Uint8Array([0, 1, 2, 3]))) + const A = Hash(new Uint8Array([0, 1, 2, 3])) + const B = Hash(new Uint8Array([0, 2, 2, 3])) + Assert.NotEqual(A, B) + }) + it('Should hash undefined', () => { + Assert.IsEqual('bigint', typeof Hash(undefined)) + const A = Hash(undefined) + const B = Hash(null) + Assert.NotEqual(A, B) + }) + it('Should hash symbol 1', () => { + Assert.IsEqual('bigint', typeof Hash(Symbol())) + const A = Hash(Symbol(1)) + const B = Hash(Symbol()) + Assert.NotEqual(A, B) + }) + it('Should hash symbol 2', () => { + Assert.IsEqual('bigint', typeof Hash(Symbol())) + const A = Hash(Symbol(1)) + const B = Hash(Symbol(2)) + Assert.NotEqual(A, B) + }) + it('Should hash symbol 2', () => { + Assert.IsEqual('bigint', typeof Hash(Symbol())) + const A = Hash(Symbol(1)) + const B = Hash(Symbol(1)) + Assert.IsEqual(A, B) + }) + it('Should hash bigint 1', () => { + Assert.IsEqual('bigint', typeof Hash(BigInt(1))) + const A = Hash(BigInt(1)) + const B = Hash(BigInt(2)) + Assert.NotEqual(A, B) + }) + it('Should hash bigint 2', () => { + Assert.IsEqual('bigint', typeof Hash(BigInt(1))) + const A = Hash(BigInt(1)) + const B = Hash(BigInt(1)) + Assert.IsEqual(A, B) + }) + // ---------------------------------------------------------------- + // Unicode + // ---------------------------------------------------------------- + it('Should hash unicode 1 (retain single byte hash)', () => { + const hash = Hash('a') + Assert.IsEqual(hash, 586962220959696054n) + }) + it('Should hash unicode 2', () => { + const hash = Hash('안녕 세계') + Assert.IsEqual(hash, 11219208047802711777n) + }) +}) diff --git a/test/runtime/value/hash/index.ts b/test/runtime/value/hash/index.ts new file mode 100644 index 000000000..50fbad4e5 --- /dev/null +++ b/test/runtime/value/hash/index.ts @@ -0,0 +1 @@ +import './hash' diff --git a/test/runtime/value/index.ts b/test/runtime/value/index.ts new file mode 100644 index 000000000..515d5fff8 --- /dev/null +++ b/test/runtime/value/index.ts @@ -0,0 +1,16 @@ +import './assert' +import './cast' +import './check' +import './clean' +import './clone' +import './convert' +import './create' +import './default' +import './delta' +import './equal' +import './guard' +import './hash' +import './mutate' +import './parse' +import './pointer' +import './transform' diff --git a/test/runtime/value/mutate/index.ts b/test/runtime/value/mutate/index.ts new file mode 100644 index 000000000..28e30125c --- /dev/null +++ b/test/runtime/value/mutate/index.ts @@ -0,0 +1 @@ +import './mutate' diff --git a/test/runtime/value/mutate/mutate.ts b/test/runtime/value/mutate/mutate.ts new file mode 100644 index 000000000..db4d65661 --- /dev/null +++ b/test/runtime/value/mutate/mutate.ts @@ -0,0 +1,116 @@ +import { Value } from '@sinclair/typebox/value' +import { Assert } from '../../assert/index' + +describe('value/mutate/Mutate', () => { + // -------------------------------------------- + // Throw + // -------------------------------------------- + it('should throw 1', () => { + // @ts-ignore + Assert.Throws(() => Value.Mutate(1, 1)) + }) + it('should throw 2', () => { + // @ts-ignore + Assert.Throws(() => Value.Mutate({}, 1)) + }) + it('should throw 3', () => { + // @ts-ignore + Assert.Throws(() => Value.Mutate([], 1)) + }) + it('should throw 4', () => { + // @ts-ignore + Assert.Throws(() => Value.Mutate({}, [])) + }) + it('should throw 5', () => { + // @ts-ignore + Assert.Throws(() => Value.Mutate([], {})) + }) + // -------------------------------------------- + // Mutate + // -------------------------------------------- + it('Should mutate 0', () => { + const Y = { z: 1 } + const X = { y: Y } + const A = { x: X } + Value.Mutate(A, {}) + Assert.IsEqual(A, {}) + }) + it('Should mutate 1', () => { + const Y = { z: 1 } + const X = { y: Y } + const A = { x: X } + Value.Mutate(A, { x: { y: { z: 2 } } }) + Assert.IsEqual(A.x.y.z, 2) + Assert.IsEqual(A.x.y, Y) + Assert.IsEqual(A.x, X) + }) + it('Should mutate 2', () => { + const Y = { z: 1 } + const X = { y: Y } + const A = { x: X } + Value.Mutate(A, { x: { y: { z: [1, 2, 3] } } }) + Assert.IsEqual(A.x.y.z, [1, 2, 3]) + Assert.IsEqual(A.x.y, Y) + Assert.IsEqual(A.x, X) + }) + it('Should mutate 3', () => { + const Y = { z: 1 } + const X = { y: Y } + const A = { x: X } + Value.Mutate(A, { x: {} }) + Assert.IsEqual(A.x.y, undefined) + Assert.IsEqual(A.x, X) + }) + it('Should mutate 4', () => { + const Y = { z: 1 } + const X = { y: Y } + const A = { x: X } + Value.Mutate(A, { x: { y: 1 } }) + Assert.IsEqual(A.x.y, 1) + Assert.IsEqual(A.x, X) + }) + it('Should mutate 5', () => { + const Y = { z: 1 } + const X = { y: Y } + const A = { x: X } + Value.Mutate(A, { x: { y: [1, 2, 3] } }) + Assert.IsEqual(A.x.y, [1, 2, 3]) + Assert.IsEqual(A.x, X) + }) + it('Should mutate 6', () => { + const Y = { z: 1 } + const X = { y: Y } + const A = { x: X } + Value.Mutate(A, { x: [1, 2, 3] }) + Assert.NotEqual(A.x, X) + Assert.IsEqual(A.x, [1, 2, 3]) + }) + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/1119 + // ---------------------------------------------------------------- + it('Should mutate array 1', () => { + const A: unknown[] = [] + Value.Mutate(A, []) + Assert.IsEqual(A, []) + }) + it('Should mutate array 2', () => { + const A: unknown[] = [] + Value.Mutate(A, [1]) + Assert.IsEqual(A, [1]) + }) + it('Should mutate array 3', () => { + const A: unknown[] = [1, 2, 3] + Value.Mutate(A, [1, 2]) + Assert.IsEqual(A, [1, 2]) + }) + it('Should mutate array 4', () => { + const A: unknown[] = [1, 2, 3] + Value.Mutate(A, [1, 2, 3, 4]) + Assert.IsEqual(A, [1, 2, 3, 4]) + }) + it('Should mutate array 5', () => { + const A: unknown[] = [1, 2, 3] + Value.Mutate(A, [{}, {}, {}, [1, 2, 3]]) + Assert.IsEqual(A, [{}, {}, {}, [1, 2, 3]]) + }) +}) diff --git a/test/runtime/value/parse/index.ts b/test/runtime/value/parse/index.ts new file mode 100644 index 000000000..09cc87100 --- /dev/null +++ b/test/runtime/value/parse/index.ts @@ -0,0 +1 @@ +import './parse' diff --git a/test/runtime/value/parse/parse.ts b/test/runtime/value/parse/parse.ts new file mode 100644 index 000000000..2bf7106de --- /dev/null +++ b/test/runtime/value/parse/parse.ts @@ -0,0 +1,125 @@ +import { Value, AssertError, ParseRegistry } from '@sinclair/typebox/value' +import { Type, TypeGuard } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +// prettier-ignore +describe('value/Parse', () => { + it('Should Parse', () => { + const A = Value.Parse(Type.Literal('hello'), 'hello') + Assert.IsEqual(A, 'hello') + }) + it('Should not Parse', () => { + Assert.Throws(() => Value.Parse(Type.Literal('hello'), 'world')) + }) + it('Should throw AssertError', () => { + try { + Value.Parse(Type.Literal('hello'), 'world') + } catch(error) { + if(error instanceof AssertError) { + return + } + throw error + } + }) + it('Should throw AssertError and produce Iterator', () => { + try { + Value.Parse(Type.Literal('hello'), 'world') + } catch(error) { + if(error instanceof AssertError) { + const first = error.Errors().First() + Assert.HasProperty(first, 'type') + Assert.HasProperty(first, 'schema') + Assert.HasProperty(first, 'path') + Assert.HasProperty(first, 'value') + Assert.HasProperty(first, 'message') + return + } + throw error + } + }) + // ---------------------------------------------------------------- + // Default + // ---------------------------------------------------------------- + it('Should use Default values', () => { + const X = Value.Parse(Type.Object({ + x: Type.Number({ default: 1 }), + y: Type.Number({ default: 2 }) + }), { }) + Assert.IsEqual(X, { x: 1, y: 2 }) + }) + it('Should throw on invalid Default values', () => { + Assert.Throws(() => Value.Parse(Type.Object({ + x: Type.Number({ default: null }), + y: Type.Number({ default: null }) + }), { })) + }) + // ---------------------------------------------------------------- + // Convert + // ---------------------------------------------------------------- + it('Should Convert value 1', () => { + const X = Value.Parse(Type.Object({ + x: Type.Number(), + y: Type.Number() + }), { x: '1', y: '2' }) + Assert.IsEqual(X, { x: 1, y: 2 }) + }) + it('Should Convert value 2', () => { + const X = Value.Parse(Type.Array(Type.String()), [1, 2, 3, 4]) + Assert.IsEqual(X, ['1', '2', '3', '4']) + }) + // ---------------------------------------------------------------- + // Clean + // ---------------------------------------------------------------- + it('Should Clean value', () => { + const X = Value.Parse(Type.Object({ + x: Type.Number(), + y: Type.Number() + }), { x: 1, y: 2, z: 3 }) + Assert.IsEqual(X, { x: 1, y: 2 }) + }) + // ---------------------------------------------------------------- + // Decode + // ---------------------------------------------------------------- + it('Should Decode value', () => { + const T = Type.Transform(Type.String()) + .Decode(value => 'hello') + .Encode(value => value) + const X = Value.Parse(T, 'world') + Assert.IsEqual(X, 'hello') + }) + // ---------------------------------------------------------------- + // Operations + // ---------------------------------------------------------------- + it('Should run operations 1', () => { + const A = Type.Object({ x: Type.Number() }) + const I = { x: 1 } + const O = Value.Parse([], A, I) + Assert.IsTrue(I === O) + }) + it('Should run operations 2', () => { + const A = Type.Object({ x: Type.Number() }) + const I = { x: 1 } + const O = Value.Parse(['Clone'], A, I) + Assert.IsTrue(I !== O) + }) + it('Should run operations 3', () => { + ParseRegistry.Set('Intercept', ( schema, references, value) => { throw 1 }) + const A = Type.Object({ x: Type.Number() }) + Assert.Throws(() => Value.Parse(['Intercept'], A, null)) + ParseRegistry.Delete('Intercept') + const F = ParseRegistry.Get('Intercept') + Assert.IsEqual(F, undefined) + }) + it('Should run operations 4', () => { + ParseRegistry.Set('Intercept', ( schema, references, value) => { + Assert.IsEqual(value, 12345) + Assert.IsTrue(TypeGuard.IsNumber(schema)) + Assert.IsTrue(TypeGuard.IsString(references[0])) + }) + Value.Parse(['Intercept'], Type.Number(), [Type.String()], 12345) + ParseRegistry.Delete('Intercept') + }) + it('Should run operations 5', () => { + Assert.Throws(() => Value.Parse(['Intercept'], Type.String(), null)) + }) +}) diff --git a/test/runtime/value/pointer/index.ts b/test/runtime/value/pointer/index.ts new file mode 100644 index 000000000..a4004f3be --- /dev/null +++ b/test/runtime/value/pointer/index.ts @@ -0,0 +1 @@ +import './pointer' diff --git a/test/runtime/value/pointer/pointer.ts b/test/runtime/value/pointer/pointer.ts new file mode 100644 index 000000000..d359716eb --- /dev/null +++ b/test/runtime/value/pointer/pointer.ts @@ -0,0 +1,342 @@ +import { ValuePointer } from '@sinclair/typebox/value' +import { Assert } from '../../assert/index' + +describe('value/pointer/Pointer', () => { + //----------------------------------------------- + // Format + //----------------------------------------------- + it('Should produce correct format #1', () => { + const R = [...ValuePointer.Format('')] + Assert.IsEqual(R, []) + }) + it('Should produce correct format #2', () => { + const R = [...ValuePointer.Format('a')] + Assert.IsEqual(R, ['a']) + }) + it('Should produce correct format #3', () => { + const R = [...ValuePointer.Format('/')] + Assert.IsEqual(R, ['']) + }) + it('Should produce correct format #4', () => { + const R = [...ValuePointer.Format('/x')] + Assert.IsEqual(R, ['x']) + }) + it('Should produce correct format #5', () => { + const R = [...ValuePointer.Format('/x/')] + Assert.IsEqual(R, ['x', '']) + }) + it('Should produce correct format #6', () => { + const R = [...ValuePointer.Format('/x//')] + Assert.IsEqual(R, ['x', '', '']) + }) + it('Should produce correct format #7', () => { + const R = [...ValuePointer.Format('/x//y')] + Assert.IsEqual(R, ['x', '', 'y']) + }) + it('Should produce correct format #8', () => { + const R = [...ValuePointer.Format('/x//y/')] + Assert.IsEqual(R, ['x', '', 'y', '']) + }) + it('Should produce correct format #9', () => { + const R = [...ValuePointer.Format('/x/~0')] + Assert.IsEqual(R, ['x', '~']) + }) + it('Should produce correct format #10', () => { + const R = [...ValuePointer.Format('/x/~1')] + Assert.IsEqual(R, ['x', '/']) + }) + it('Should produce correct format #11', () => { + const R = [...ValuePointer.Format('/x/~0/')] + Assert.IsEqual(R, ['x', '~', '']) + }) + it('Should produce correct format #12', () => { + const R = [...ValuePointer.Format('/x/~1/')] + Assert.IsEqual(R, ['x', '/', '']) + }) + it('Should produce correct format #13', () => { + const R = [...ValuePointer.Format('/x/a~0b')] + Assert.IsEqual(R, ['x', 'a~b']) + }) + it('Should produce correct format #14', () => { + const R = [...ValuePointer.Format('/x/a~1b')] + Assert.IsEqual(R, ['x', 'a/b']) + }) + it('Should produce correct format #15', () => { + const R = [...ValuePointer.Format('/x/a~0b/')] + Assert.IsEqual(R, ['x', 'a~b', '']) + }) + it('Should produce correct format #16', () => { + const R = [...ValuePointer.Format('/x/a~1b/')] + Assert.IsEqual(R, ['x', 'a/b', '']) + }) + it('Should produce correct format #17', () => { + const R = [...ValuePointer.Format('/x/a~0b///y')] + Assert.IsEqual(R, ['x', 'a~b', '', '', 'y']) + }) + it('Should produce correct format #18', () => { + const R = [...ValuePointer.Format('/x/a~1b///y')] + Assert.IsEqual(R, ['x', 'a/b', '', '', 'y']) + }) + it('Should produce correct format #19', () => { + const R = [...ValuePointer.Format('/x/a~0b///')] + Assert.IsEqual(R, ['x', 'a~b', '', '', '']) + }) + it('Should produce correct format #20', () => { + const R = [...ValuePointer.Format('/x/a~1b///')] + Assert.IsEqual(R, ['x', 'a/b', '', '', '']) + }) + //----------------------------------------------- + // Get + //----------------------------------------------- + it('Should get array #1', () => { + const V = [0, 1, 2, 3] + Assert.IsEqual(ValuePointer.Get(V, ''), [0, 1, 2, 3]) + Assert.IsEqual(ValuePointer.Get(V, '/'), undefined) + Assert.IsEqual(ValuePointer.Get(V, '/0'), 0) + Assert.IsEqual(ValuePointer.Get(V, '/1'), 1) + Assert.IsEqual(ValuePointer.Get(V, '/2'), 2) + Assert.IsEqual(ValuePointer.Get(V, '/3'), 3) + }) + it('Should get array #2', () => { + const V = [{ x: 0 }, { x: 1 }, { x: 2 }, { x: 3 }] + Assert.IsEqual(ValuePointer.Get(V, ''), [{ x: 0 }, { x: 1 }, { x: 2 }, { x: 3 }]) + Assert.IsEqual(ValuePointer.Get(V, '/'), undefined) + Assert.IsEqual(ValuePointer.Get(V, '/0'), { x: 0 }) + Assert.IsEqual(ValuePointer.Get(V, '/1'), { x: 1 }) + Assert.IsEqual(ValuePointer.Get(V, '/2'), { x: 2 }) + Assert.IsEqual(ValuePointer.Get(V, '/3'), { x: 3 }) + Assert.IsEqual(ValuePointer.Get(V, '/0/x'), 0) + Assert.IsEqual(ValuePointer.Get(V, '/1/x'), 1) + Assert.IsEqual(ValuePointer.Get(V, '/2/x'), 2) + Assert.IsEqual(ValuePointer.Get(V, '/3/x'), 3) + }) + it('Should get object #1', () => { + const V = { x: 0, y: 1, z: 2 } + Assert.IsEqual(ValuePointer.Get(V, ''), { x: 0, y: 1, z: 2 }) + Assert.IsEqual(ValuePointer.Get(V, '/'), undefined) + Assert.IsEqual(ValuePointer.Get(V, '/x'), 0) + Assert.IsEqual(ValuePointer.Get(V, '/y'), 1) + Assert.IsEqual(ValuePointer.Get(V, '/z'), 2) + }) + it('Should get object #2', () => { + const V = { x: { x: 0 }, y: { x: 1 }, z: { x: 2 } } + Assert.IsEqual(ValuePointer.Get(V, ''), { x: { x: 0 }, y: { x: 1 }, z: { x: 2 } }) + Assert.IsEqual(ValuePointer.Get(V, '/'), undefined) + Assert.IsEqual(ValuePointer.Get(V, '/x'), { x: 0 }) + Assert.IsEqual(ValuePointer.Get(V, '/y'), { x: 1 }) + Assert.IsEqual(ValuePointer.Get(V, '/z'), { x: 2 }) + }) + it('Should get object #3', () => { + const V = { '': { x: -1 }, x: { '': { x: 1 } }, y: { '': { x: 2 } }, z: { '': { x: 3 } } } + Assert.IsEqual(ValuePointer.Get(V, ''), { '': { x: -1 }, x: { '': { x: 1 } }, y: { '': { x: 2 } }, z: { '': { x: 3 } } }) + Assert.IsEqual(ValuePointer.Get(V, '/'), { x: -1 }) + Assert.IsEqual(ValuePointer.Get(V, '/x'), { '': { x: 1 } }) + Assert.IsEqual(ValuePointer.Get(V, '/y'), { '': { x: 2 } }) + Assert.IsEqual(ValuePointer.Get(V, '/z'), { '': { x: 3 } }) + Assert.IsEqual(ValuePointer.Get(V, '/x/'), { x: 1 }) + Assert.IsEqual(ValuePointer.Get(V, '/y/'), { x: 2 }) + Assert.IsEqual(ValuePointer.Get(V, '/z/'), { x: 3 }) + Assert.IsEqual(ValuePointer.Get(V, '/x//x'), 1) + Assert.IsEqual(ValuePointer.Get(V, '/y//x'), 2) + Assert.IsEqual(ValuePointer.Get(V, '/z//x'), 3) + }) + //----------------------------------------------- + // Has + //----------------------------------------------- + it('Should return has true for undefined', () => { + const V = undefined + Assert.IsEqual(ValuePointer.Has(V, ''), true) + }) + it('Should return has true for null', () => { + const V = null + Assert.IsEqual(ValuePointer.Has(V, ''), true) + }) + it('Should return has true for object', () => { + const V = {} + Assert.IsEqual(ValuePointer.Has(V, ''), true) + }) + it('Should return has true for array', () => { + const V: any[] = [] + Assert.IsEqual(ValuePointer.Has(V, ''), true) + }) + it('Should return has true for string', () => { + const V = 'hello' + Assert.IsEqual(ValuePointer.Has(V, ''), true) + }) + it('Should return has true for number', () => { + const V = 42 + Assert.IsEqual(ValuePointer.Has(V, ''), true) + }) + it('Should return has true for boolean', () => { + const V = false + Assert.IsEqual(ValuePointer.Has(V, ''), true) + }) + it('Should return has true for deeply nested', () => { + const V = { + '': { x: { y: { z: 1 } } }, + x: 1, + y: { x: 1 }, + z: [{ x: 1 }, { y: 1 }], + w: undefined, + n: null, + } + // exists + Assert.IsEqual(ValuePointer.Has(V, ''), true) + Assert.IsEqual(ValuePointer.Has(V, '/'), true) + Assert.IsEqual(ValuePointer.Has(V, '//x'), true) + Assert.IsEqual(ValuePointer.Has(V, '//x/y'), true) + Assert.IsEqual(ValuePointer.Has(V, '//x/y/z'), true) + Assert.IsEqual(ValuePointer.Has(V, '/x'), true) + Assert.IsEqual(ValuePointer.Has(V, '/y/x'), true) + Assert.IsEqual(ValuePointer.Has(V, '/z'), true) + Assert.IsEqual(ValuePointer.Has(V, '/z/0'), true) + Assert.IsEqual(ValuePointer.Has(V, '/z/0/x'), true) + Assert.IsEqual(ValuePointer.Has(V, '/z/1'), true) + Assert.IsEqual(ValuePointer.Has(V, '/z/1/y'), true) + Assert.IsEqual(ValuePointer.Has(V, '/x'), true) + Assert.IsEqual(ValuePointer.Has(V, '/n'), true) + }) + //----------------------------------------------- + // Set + //----------------------------------------------- + it('Should throw when setting root', () => { + const V = {} + Assert.Throws(() => ValuePointer.Set(V, '', { x: 1 })) + }) + it('Should set array values', () => { + const V = [0, 1, 2] + ValuePointer.Set(V, '/0', 3) + ValuePointer.Set(V, '/1', 4) + ValuePointer.Set(V, '/2', 5) + Assert.IsEqual(V, [3, 4, 5]) + }) + it('Should set object values', () => { + const V = { x: 0, y: 1, z: 2 } + ValuePointer.Set(V, '/x', 3) + ValuePointer.Set(V, '/y', 4) + ValuePointer.Set(V, '/z', 5) + Assert.IsEqual(V, { x: 3, y: 4, z: 5 }) + }) + it('Should set object values recursively #1', () => { + const V = {} + ValuePointer.Set(V, '/x/y/z', 1) + Assert.IsEqual(V, { x: { y: { z: 1 } } }) + }) + it('Should set object values recursively #2', () => { + const V = {} + ValuePointer.Set(V, '/x/0/y/z/', 1) + ValuePointer.Set(V, '/x/1/y/z/', 2) + Assert.IsEqual(V, { + x: { + 0: { + y: { + z: { + '': 1, + }, + }, + }, + 1: { + y: { + z: { + '': 2, + }, + }, + }, + }, + }) + }) + //----------------------------------------------- + // Delete + //----------------------------------------------- + it('Should throw when deleting root', () => { + const V = {} + Assert.Throws(() => ValuePointer.Delete(V, '')) + }) + it('Should delete object properties', () => { + const V = { + x: { x: 1, y: 2, z: 3 }, + y: { x: 3, y: 4, z: 5 }, + } + ValuePointer.Delete(V, '/x/y') + ValuePointer.Delete(V, '/y') + Assert.IsEqual(V, { x: { x: 1, z: 3 } }) + }) + it('Should be a noop if property does not exist', () => { + const V = { + x: { x: 1, y: 2, z: 3 }, + y: { x: 3, y: 4, z: 5 }, + } + ValuePointer.Delete(V, '/x/w') + ValuePointer.Delete(V, '/w') + Assert.IsEqual(V, { + x: { x: 1, y: 2, z: 3 }, + y: { x: 3, y: 4, z: 5 }, + }) + }) + it('Should not delete owner', () => { + const V = { x: { y: { z: 1 } } } + ValuePointer.Delete(V, '/x/y/z') + Assert.IsEqual(V, { x: { y: {} } }) + }) + it('Should delete owner', () => { + const V = { x: { y: { z: 1 } } } + ValuePointer.Delete(V, '/x/y') + Assert.IsEqual(V, { x: {} }) + }) + it('Should not throw if deleting null property', () => { + const V = { x: { y: null } } + ValuePointer.Delete(V, '/x/y/z') + Assert.IsEqual(V, { x: { y: null } }) + }) + //----------------------------------------------- + // Escapes + //----------------------------------------------- + it('Should support get ~0 pointer escape', () => { + const V = { + x: { '~': { x: 1 } }, + } + Assert.IsEqual(ValuePointer.Get(V, '/x/~0'), { x: 1 }) + }) + it('Should support get ~1 pointer escape', () => { + const V = { + x: { '/': { x: 1 } }, + } + Assert.IsEqual(ValuePointer.Get(V, '/x/~1'), { x: 1 }) + }) + it('Should support set ~0 pointer escape', () => { + const V = { + x: { '~': { x: 1 } }, + } + ValuePointer.Set(V, '/x/~0', { x: 2 }) + Assert.IsEqual(V, { + x: { '~': { x: 2 } }, + }) + }) + it('Should support set ~1 pointer escape', () => { + const V = { + x: { '/': { x: 1 } }, + } + ValuePointer.Set(V, '/x/~1', { x: 2 }) + Assert.IsEqual(V, { + x: { '/': { x: 2 } }, + }) + }) + it('Should support delete ~0 pointer escape', () => { + const V = { + x: { '~': { x: 1 } }, + } + ValuePointer.Delete(V, '/x/~0') + Assert.IsEqual(V, { + x: {}, + }) + }) + it('Should support delete ~1 pointer escape', () => { + const V = { + x: { '/': { x: 1 } }, + } + ValuePointer.Delete(V, '/x/~1') + Assert.IsEqual(V, { + x: {}, + }) + }) +}) diff --git a/test/runtime/value/transform/_encoder.ts b/test/runtime/value/transform/_encoder.ts new file mode 100644 index 000000000..22a2b1d1d --- /dev/null +++ b/test/runtime/value/transform/_encoder.ts @@ -0,0 +1,36 @@ +import { IsAsyncIterator, IsIterator, IsFunction, IsSymbol, IsDate } from '@sinclair/typebox/value' +import { TSchema, StaticDecode, StaticEncode } from '@sinclair/typebox' +import { TypeCompiler } from '@sinclair/typebox/compiler' +import { Value } from '@sinclair/typebox/value' +import { Assert } from '../../assert/index' + +function AssertSame(actual: unknown, expect: unknown) { + if (IsAsyncIterator(actual) && IsAsyncIterator(expect)) return + if (IsIterator(actual) && IsIterator(expect)) return + if (IsSymbol(actual) && IsSymbol(expect)) return + if (IsFunction(actual) && IsFunction(expect)) return + if (IsDate(actual) && IsDate(expect)) { + return Assert.IsEqual(actual.getTime(), expect.getTime()) + } + Assert.IsEqual(actual, expect) +} + +export function Decode>(schema: T, references: TSchema[], value: unknown): R +export function Decode>(schema: T, value: unknown): R +export function Decode(...args: any[]) { + const [schema, references, value] = args.length === 2 ? [args[0], [], args[1]] : [args[0], args[1], args[2]] + const value1 = TypeCompiler.Compile(schema as TSchema, references).Decode(value) + const value2 = Value.Decode(schema as TSchema, references, value) + AssertSame(value1, value2) + return value2 +} + +export function Encode>(schema: T, references: TSchema[], value: unknown): R +export function Encode>(schema: T, value: unknown): R +export function Encode(...args: any[]) { + const [schema, references, value] = args.length === 2 ? [args[0], [], args[1]] : [args[0], args[1], args[2]] + const value1 = TypeCompiler.Compile(schema as TSchema, references).Encode(value) + const value2 = Value.Encode(schema as TSchema, references, value) + AssertSame(value1, value2) + return value2 +} diff --git a/test/runtime/value/transform/_nested.ts b/test/runtime/value/transform/_nested.ts new file mode 100644 index 000000000..700de2c38 --- /dev/null +++ b/test/runtime/value/transform/_nested.ts @@ -0,0 +1,52 @@ +import { Assert } from '../../assert' +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' + +describe('value/transform/Nested', () => { + // -------------------------------------------------------- + // Identity + // -------------------------------------------------------- + const T11 = Type.Transform(Type.Literal(1)) + .Decode((value) => value) + .Encode((value) => value) + const T12 = Type.Transform(T11) + .Decode((value) => value) + .Encode((value) => value) + const T13 = Type.Transform(T12) + .Decode((value) => value) + .Encode((value) => value) + it('Should decode identity', () => { + const R = Value.Decode(T13, 1) + Assert.IsEqual(R, 1) + }) + it('Should encode identity', () => { + const R = Value.Encode(T13, 1) + Assert.IsEqual(R, 1) + }) + it('Should throw on identity decode', () => { + Assert.Throws(() => Value.Decode(T13, null)) + }) + // -------------------------------------------------------- + // Mapped + // -------------------------------------------------------- + const T21 = Type.Transform(Type.Literal(1)) + .Decode((value) => 2 as const) + .Encode((value) => 1 as const) + const T22 = Type.Transform(T21) + .Decode((value) => 3 as const) + .Encode((value) => 2 as const) + const T23 = Type.Transform(T22) + .Decode((value) => 4 as const) + .Encode((value) => 3 as const) + it('Should decode mapped', () => { + const R = Value.Decode(T23, 1) + Assert.IsEqual(R, 4) + }) + it('Should encode mapped', () => { + const R = Value.Encode(T23, 4) + Assert.IsEqual(R, 1) + }) + it('Should throw on mapped decode', () => { + Assert.Throws(() => Value.Decode(T23, null)) + }) +}) diff --git a/test/runtime/value/transform/any.ts b/test/runtime/value/transform/any.ts new file mode 100644 index 000000000..a1b27e28c --- /dev/null +++ b/test/runtime/value/transform/any.ts @@ -0,0 +1,35 @@ +import * as Encoder from './_encoder' +import { Assert } from '../../assert' +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' + +describe('value/transform/Any', () => { + // -------------------------------------------------------- + // Identity + // -------------------------------------------------------- + const T0 = Type.Transform(Type.Any()) + .Decode((value) => value) + .Encode((value) => value) + it('Should decode mapped', () => { + const R = Encoder.Decode(T0, 123) + Assert.IsEqual(R, 123) + }) + it('Should encode mapped', () => { + const R = Encoder.Encode(T0, 123) + Assert.IsEqual(R, 123) + }) + // -------------------------------------------------------- + // Mapped + // -------------------------------------------------------- + const T1 = Type.Transform(Type.Any()) + .Decode((value) => 1) + .Encode((value) => 2) + it('Should decode mapped', () => { + const R = Encoder.Decode(T1, null) + Assert.IsEqual(R, 1) + }) + it('Should encode mapped', () => { + const R = Encoder.Encode(T1, null) + Assert.IsEqual(R, 2) + }) +}) diff --git a/test/runtime/value/transform/array.ts b/test/runtime/value/transform/array.ts new file mode 100644 index 000000000..8f688b2e8 --- /dev/null +++ b/test/runtime/value/transform/array.ts @@ -0,0 +1,98 @@ +import * as Encoder from './_encoder' +import { Assert } from '../../assert' +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' + +describe('value/transform/Array', () => { + // -------------------------------------------------------- + // Identity + // -------------------------------------------------------- + const T0 = Type.Transform(Type.Array(Type.Number())) + .Decode((value) => value) + .Encode((value) => value) + it('Should decode mapped', () => { + const R = Encoder.Decode(T0, [0, 1, 2]) + Assert.IsEqual(R, [0, 1, 2]) + }) + it('Should encode mapped', () => { + const R = Encoder.Encode(T0, [0, 1, 2]) + Assert.IsEqual(R, [0, 1, 2]) + }) + it('Should throw on mapped decode', () => { + Assert.Throws(() => Encoder.Decode(T0, null)) + }) + // -------------------------------------------------------- + // Mapped + // -------------------------------------------------------- + const T1 = Type.Transform(Type.Array(Type.Number())) + .Decode((value) => 1) + .Encode((value) => [0, 1, 2]) + it('Should decode mapped', () => { + const R = Encoder.Decode(T1, []) + Assert.IsEqual(R, 1) + }) + it('Should encode mapped', () => { + const R = Encoder.Encode(T1, null) + Assert.IsEqual(R, [0, 1, 2]) + }) + it('Should throw on mapped decode', () => { + Assert.Throws(() => Encoder.Decode(T1, null)) + }) + // -------------------------------------------------------- + // Elements + // -------------------------------------------------------- + const B2 = Type.Transform(Type.Boolean()) + .Decode((value) => (value ? 'TRUE' : 'FALSE')) + .Encode((value) => (value === 'TRUE' ? true : false)) + const T2 = Type.Array(B2) + it('Should decode elements', () => { + const R = Encoder.Decode(T2, [true, false]) + Assert.IsEqual(R, ['TRUE', 'FALSE']) + }) + it('Should encode elements', () => { + const R = Encoder.Encode(T2, ['TRUE', 'FALSE']) + Assert.IsEqual(R, [true, false]) + }) + it('Should throw on elements decode', () => { + Assert.Throws(() => Encoder.Decode(T2, null)) + }) + // -------------------------------------------------------- + // Elements Contains (Not Supported) + // -------------------------------------------------------- + const N3 = Type.Transform(Type.Literal(1)) + .Decode((value) => 'hello') + .Encode((value) => 1 as const) + const T3 = Type.Array(Type.Number(), { + contains: N3, + }) + it('Should decode contains', () => { + const R = Encoder.Decode(T3, [1, 2, 3]) + Assert.IsEqual(R, [1, 2, 3]) + }) + it('Should throw on contains encode', () => { + Assert.Throws(() => Encoder.Encode(T3, ['hello', 2, 3])) + }) + it('Should throw on contains decode', () => { + Assert.Throws(() => Encoder.Decode(T3, null)) + }) + // ------------------------------------------------------------ + // Set + // ------------------------------------------------------------ + const T4 = Type.Transform(Type.Array(Type.Number())) + .Decode((value) => new Set(value)) + .Encode((value) => [...value]) + it('should decode set', () => { + const R = Encoder.Decode(T4, [1, 1, 2, 3]) + Assert.IsInstanceOf(R, Set) + Assert.IsTrue(R.has(1)) + Assert.IsTrue(R.has(2)) + Assert.IsTrue(R.has(3)) + }) + it('should encode set', () => { + const R = Encoder.Encode(T4, new Set([1, 2, 3])) + Assert.IsEqual(R, [1, 2, 3]) + }) + it('Should throw on set decode', () => { + Assert.Throws(() => Encoder.Decode(T4, {})) + }) +}) diff --git a/test/runtime/value/transform/async-iterator.ts b/test/runtime/value/transform/async-iterator.ts new file mode 100644 index 000000000..aed1f3eb3 --- /dev/null +++ b/test/runtime/value/transform/async-iterator.ts @@ -0,0 +1,41 @@ +import * as Encoder from './_encoder' +import { Assert } from '../../assert' +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' + +describe('value/transform/AsyncIterator', () => { + // -------------------------------------------------------- + // Identity + // -------------------------------------------------------- + const T0 = Type.Transform(Type.AsyncIterator(Type.Number())) + .Decode((value) => value) + .Encode((value) => value) + it('Should decode identity', () => { + const R = Encoder.Decode(T0, (async function* (): any {})()) + Assert.IsTrue(Symbol.asyncIterator in R) + }) + it('Should encode identity', () => { + const R = Encoder.Encode(T0, (async function* (): any {})()) + Assert.IsTrue(Symbol.asyncIterator in R) + }) + it('Should throw on identity decode', () => { + Assert.Throws(() => Encoder.Decode(T0, null)) + }) + // -------------------------------------------------------- + // Mapped + // -------------------------------------------------------- + const T1 = Type.Transform(Type.AsyncIterator(Type.Number())) + .Decode((value) => 1) + .Encode((value) => (async function* (): any {})()) + it('Should decode', () => { + const R = Encoder.Decode(T1, (async function* (): any {})()) + Assert.IsEqual(R, 1) + }) + it('Should encode', () => { + const R = Encoder.Encode(T1, null) + Assert.IsTrue(Symbol.asyncIterator in R) + }) + it('Should throw on decode', () => { + Assert.Throws(() => Encoder.Decode(T1, null)) + }) +}) diff --git a/test/runtime/value/transform/bigint.ts b/test/runtime/value/transform/bigint.ts new file mode 100644 index 000000000..280af3114 --- /dev/null +++ b/test/runtime/value/transform/bigint.ts @@ -0,0 +1,41 @@ +import * as Encoder from './_encoder' +import { Assert } from '../../assert' +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' + +describe('value/transform/BigInt', () => { + // -------------------------------------------------------- + // Identity + // -------------------------------------------------------- + const T0 = Type.Transform(Type.BigInt()) + .Decode((value) => value) + .Encode((value) => value) + it('Should decode identity', () => { + const R = Encoder.Decode(T0, 5n) + Assert.IsEqual(R, 5n) + }) + it('Should encode identity', () => { + const R = Encoder.Encode(T0, 5n) + Assert.IsEqual(R, 5n) + }) + it('Should throw on identity decode', () => { + Assert.Throws(() => Encoder.Decode(T0, null)) + }) + // -------------------------------------------------------- + // Mapped + // -------------------------------------------------------- + const T1 = Type.Transform(Type.BigInt()) + .Decode((value) => 1) + .Encode((value) => 2n) + it('Should decode mapped', () => { + const R = Encoder.Decode(T1, 1n) + Assert.IsEqual(R, 1) + }) + it('Should encode mapped', () => { + const R = Encoder.Encode(T1, null) + Assert.IsEqual(R, 2n) + }) + it('Should throw on mapped decode', () => { + Assert.Throws(() => Encoder.Decode(T1, null)) + }) +}) diff --git a/test/runtime/value/transform/boolean.ts b/test/runtime/value/transform/boolean.ts new file mode 100644 index 000000000..ed9c47cee --- /dev/null +++ b/test/runtime/value/transform/boolean.ts @@ -0,0 +1,41 @@ +import * as Encoder from './_encoder' +import { Assert } from '../../assert' +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' + +describe('value/transform/Boolean', () => { + // -------------------------------------------------------- + // Identity + // -------------------------------------------------------- + const T0 = Type.Transform(Type.Boolean()) + .Decode((value) => value) + .Encode((value) => value) + it('Should decode identity', () => { + const R = Encoder.Decode(T0, true) + Assert.IsEqual(R, true) + }) + it('Should encode identity', () => { + const R = Encoder.Encode(T0, false) + Assert.IsEqual(R, false) + }) + it('Should throw on identity decode', () => { + Assert.Throws(() => Encoder.Decode(T0, null)) + }) + // -------------------------------------------------------- + // Mapped + // -------------------------------------------------------- + const T1 = Type.Transform(Type.Boolean()) + .Decode((value) => 1) + .Encode((value) => true) + it('Should decode mapped', () => { + const R = Encoder.Decode(T1, true) + Assert.IsEqual(R, 1) + }) + it('Should encode mapped', () => { + const R = Encoder.Encode(T1, null) + Assert.IsEqual(R, true) + }) + it('Should throw on mapped decode', () => { + Assert.Throws(() => Encoder.Decode(T1, null)) + }) +}) diff --git a/test/runtime/value/transform/constructor.ts b/test/runtime/value/transform/constructor.ts new file mode 100644 index 000000000..67ae9d19a --- /dev/null +++ b/test/runtime/value/transform/constructor.ts @@ -0,0 +1,41 @@ +import * as Encoder from './_encoder' +import { Assert } from '../../assert' +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' + +describe('value/transform/Constructor', () => { + // -------------------------------------------------------- + // Identity + // -------------------------------------------------------- + const T0 = Type.Transform(Type.Constructor([], Type.Any())) + .Decode((value) => value) + .Encode((value) => value) + it('Should decode identity', () => { + const R = Encoder.Decode(T0, class {}) + Assert.IsTypeOf(R, 'function') + }) + it('Should encode identity', () => { + const R = Encoder.Encode(T0, class {}) + Assert.IsTypeOf(R, 'function') + }) + it('Should throw on identity decode', () => { + Assert.Throws(() => Encoder.Decode(T0, null)) + }) + // -------------------------------------------------------- + // Mapped + // -------------------------------------------------------- + const T1 = Type.Transform(Type.Constructor([], Type.Any())) + .Decode((value) => 1) + .Encode((value) => class {}) + it('Should decode mapped', () => { + const R = Encoder.Decode(T1, class {}) + Assert.IsEqual(R, 1) + }) + it('Should encode mapped', () => { + const R = Encoder.Encode(T1, null) + Assert.IsTypeOf(R, 'function') + }) + it('Should throw on mapped decode', () => { + Assert.Throws(() => Encoder.Decode(T1, null)) + }) +}) diff --git a/test/runtime/value/transform/date.ts b/test/runtime/value/transform/date.ts new file mode 100644 index 000000000..e81ff7a89 --- /dev/null +++ b/test/runtime/value/transform/date.ts @@ -0,0 +1,41 @@ +import * as Encoder from './_encoder' +import { Assert } from '../../assert' +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' + +describe('value/transform/Date', () => { + // -------------------------------------------------------- + // Identity + // -------------------------------------------------------- + const T0 = Type.Transform(Type.Date()) + .Decode((value) => value) + .Encode((value) => value) + it('Should decode identity', () => { + const R = Encoder.Decode(T0, new Date(123)) + Assert.IsEqual(R, new Date(123)) + }) + it('Should encode identity', () => { + const R = Encoder.Encode(T0, new Date(123)) + Assert.IsEqual(R, new Date(123)) + }) + it('Should throw on identity decode', () => { + Assert.Throws(() => Encoder.Decode(T0, null)) + }) + // -------------------------------------------------------- + // Mapped + // -------------------------------------------------------- + const T1 = Type.Transform(Type.Date()) + .Decode((value) => 1) + .Encode((value) => new Date(0)) + it('Should decode mapped', () => { + const R = Encoder.Decode(T1, new Date(0)) + Assert.IsEqual(R, 1) + }) + it('Should encode mapped', () => { + const R = Encoder.Encode(T1, null) + Assert.IsInstanceOf(R, Date) + }) + it('Should throw on mapped decode', () => { + Assert.Throws(() => Encoder.Decode(T1, null)) + }) +}) diff --git a/test/runtime/value/transform/enum.ts b/test/runtime/value/transform/enum.ts new file mode 100644 index 000000000..b681ae33b --- /dev/null +++ b/test/runtime/value/transform/enum.ts @@ -0,0 +1,46 @@ +import * as Encoder from './_encoder' +import { Assert } from '../../assert' +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' + +describe('value/transform/Enum', () => { + enum E { + A, + B, + C, + } + // -------------------------------------------------------- + // Identity + // -------------------------------------------------------- + const T0 = Type.Transform(Type.Enum(E)) + .Decode((value) => value) + .Encode((value) => value) + it('Should decode identity', () => { + const R = Encoder.Decode(T0, E.A) + Assert.IsEqual(R, E.A) + }) + it('Should encode identity', () => { + const R = Encoder.Encode(T0, E.A) + Assert.IsEqual(R, E.A) + }) + it('Should throw on identity decode', () => { + Assert.Throws(() => Encoder.Decode(T0, null)) + }) + // -------------------------------------------------------- + // Mapped + // -------------------------------------------------------- + const T1 = Type.Transform(Type.Enum(E)) + .Decode((value) => 1) + .Encode((value) => E.A) + it('Should decode mapped', () => { + const R = Encoder.Decode(T1, E.A) + Assert.IsEqual(R, 1) + }) + it('Should encode mapped', () => { + const R = Encoder.Encode(T1, null) + Assert.IsEqual(R, E.A) + }) + it('Should throw on mapped decode', () => { + Assert.Throws(() => Encoder.Decode(T1, null)) + }) +}) diff --git a/test/runtime/value/transform/function.ts b/test/runtime/value/transform/function.ts new file mode 100644 index 000000000..4bd89abc6 --- /dev/null +++ b/test/runtime/value/transform/function.ts @@ -0,0 +1,41 @@ +import * as Encoder from './_encoder' +import { Assert } from '../../assert' +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' + +describe('value/transform/Function', () => { + // -------------------------------------------------------- + // Identity + // -------------------------------------------------------- + const T0 = Type.Transform(Type.Function([], Type.Any())) + .Decode((value) => value) + .Encode((value) => value) + it('Should decode identity', () => { + const R = Encoder.Decode(T0, class {}) + Assert.IsTypeOf(R, 'function') + }) + it('Should encode identity', () => { + const R = Encoder.Encode(T0, class {}) + Assert.IsTypeOf(R, 'function') + }) + it('Should throw on identity decode', () => { + Assert.Throws(() => Encoder.Decode(T0, null)) + }) + // -------------------------------------------------------- + // Mapped + // -------------------------------------------------------- + const T1 = Type.Transform(Type.Function([], Type.Any())) + .Decode((value) => 1) + .Encode((value) => function () {}) + it('Should decode mapped', () => { + const R = Encoder.Decode(T1, function () {}) + Assert.IsEqual(R, 1) + }) + it('Should encode mapped', () => { + const R = Encoder.Encode(T1, null) + Assert.IsTypeOf(R, 'function') + }) + it('Should throw on mapped decode', () => { + Assert.Throws(() => Encoder.Decode(T1, null)) + }) +}) diff --git a/test/runtime/value/transform/import.ts b/test/runtime/value/transform/import.ts new file mode 100644 index 000000000..456610f75 --- /dev/null +++ b/test/runtime/value/transform/import.ts @@ -0,0 +1,221 @@ +import * as Encoder from './_encoder' +import { Assert } from '../../assert' + +import { TypeSystemPolicy } from '@sinclair/typebox/system' +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' + +// prettier-ignore +describe('value/transform/Import', () => { + // -------------------------------------------------------- + // Identity + // -------------------------------------------------------- + const T0 = Type.Transform( + Type.Module({ A: Type.Object({ + x: Type.Number(), + y: Type.Number(), + })}).Import('A'), + ) + .Decode((value) => value) + .Encode((value) => value) + it('Should decode identity', () => { + const R = Encoder.Decode(T0, { x: 1, y: 2 }) + Assert.IsEqual(R, { x: 1, y: 2 }) + }) + it('Should encode identity', () => { + const R = Encoder.Encode(T0, { x: 1, y: 2 }) + Assert.IsEqual(R, { x: 1, y: 2 }) + }) + it('Should throw on identity decode', () => { + Assert.Throws(() => Encoder.Decode(T0, undefined)) + }) + // ---------------------------------------------------------- + // Object + // ---------------------------------------------------------- + const T1 = Type.Transform( + Type.Module({ A: Type.Object({ + x: Type.Number(), + y: Type.Number(), + })}).Import('A'), + ) + .Decode((value) => 42) + .Encode((value) => ({ x: 1, y: 2 })) + it('Should decode mapped', () => { + const R = Encoder.Decode(T1, { x: 1, y: 2 }) + Assert.IsEqual(R, 42) + }) + it('Should encode mapped', () => { + const R = Encoder.Encode(T1, null) + Assert.IsEqual(R, { x: 1, y: 2 }) + }) + it('Should throw on mapped decode', () => { + Assert.Throws(() => Encoder.Decode(T1, undefined)) + }) + // ---------------------------------------------------------- + // Object: Transform Property + // ---------------------------------------------------------- + const N2 = Type.Transform(Type.Module({ A: Type.Integer() }).Import('A')) + .Decode((value) => value.toString()) + .Encode((value) => parseInt(value)) + const T2 = Type.Object({ + x: N2, + y: N2, + }) + it('Should decode transform property', () => { + const R = Encoder.Decode(T2, { x: 1, y: 2 }) + Assert.IsEqual(R, { x: '1', y: '2' }) + }) + it('Should encode transform property', () => { + const R = Encoder.Encode(T2, { x: '1', y: '2' }) + Assert.IsEqual(R, { x: 1, y: 2 }) + }) + it('Should throw on decode transform property', () => { + Assert.Throws(() => Encoder.Decode(T2, undefined)) + }) + // ---------------------------------------------------------- + // Object: Transform Property Nested (Twizzle) + // ---------------------------------------------------------- + const N3 = Type.Transform(Type.Module({ A: Type.Integer() }).Import('A')) + .Decode((value) => value.toString()) + .Encode((value) => parseInt(value)) + const T3 = Type.Transform( + Type.Object({ + x: N3, + y: N3, + }), + ) + .Decode((value) => ({ x: value.y, y: value.x })) + .Encode((value) => ({ x: value.y, y: value.x })) + it('Should decode transform property nested', () => { + const R = Encoder.Decode(T3, { x: 1, y: 2 }) + Assert.IsEqual(R, { x: '2', y: '1' }) + }) + it('Should encode transform property nested', () => { + const R = Encoder.Encode(T3, { x: '1', y: '2' }) + Assert.IsEqual(R, { x: 2, y: 1 }) + }) + it('Should throw on decode transform property nested', () => { + Assert.Throws(() => Encoder.Decode(T3, undefined)) + }) + // ---------------------------------------------------------- + // Object Additional Properties + // ---------------------------------------------------------- + const N4 = Type.Transform(Type.Module({ A: Type.Integer() }).Import('A')) + .Decode((value) => value.toString()) + .Encode((value) => parseInt(value)) + const T4 = Type.Transform( + Type.Object( + { + x: Type.Number(), + }, + { + additionalProperties: N4, + }, + ), + ) + .Decode((value) => value) + .Encode((value) => value) + it('Should decode additional property', () => { + const R = Encoder.Decode(T4, { x: 1, y: 2 }) + Assert.IsEqual(R, { x: 1, y: '2' }) + }) + it('Should encode additional property', () => { + const R = Encoder.Encode(T4, { x: 1, y: '5' }) + Assert.IsEqual(R, { x: 1, y: 5 }) + }) + it('Should throw on additional property 1', () => { + Assert.Throws(() => Encoder.Decode(T4, undefined)) + }) + it('Should throw on additional property 2', () => { + Assert.Throws(() => Encoder.Decode(T4, { x: 1, y: true })) + }) + // ------------------------------------------------------------ + // Map + // ------------------------------------------------------------ + const T5 = Type.Transform(Type.Module({ A: Type.Object({ x: Type.String(), y: Type.String() })}).Import('A')) + .Decode((value) => new Map(Object.entries(value))) + .Encode((value) => Object.fromEntries(value.entries()) as any) + it('Should decode map', () => { + const R = Encoder.Decode(T5, { x: 'hello', y: 'world' }) + Assert.IsInstanceOf(R, Map) + Assert.IsEqual(R.get('x'), 'hello') + Assert.IsEqual(R.get('y'), 'world') + }) + it('Should encode map', () => { + const R = Encoder.Encode( + T5, + new Map([ + ['x', 'hello'], + ['y', 'world'], + ]), + ) + Assert.IsEqual(R, { x: 'hello', y: 'world' }) + }) + it('Should throw on map decode', () => { + Assert.Throws(() => Encoder.Decode(T5, {})) + }) + + // ------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/1178 + // ------------------------------------------------------------- + // immediate + it('Should transform embedded module codec 1', () => { + const T = Type.Module({ + A: Type.Transform(Type.String()) + .Decode((value) => parseInt(value)) + .Encode((value) => value.toString()), + }).Import('A') + + const D = Value.Decode(T, '123') + const E = Value.Encode(T, 123) + Assert.IsEqual(D, 123) + Assert.IsEqual(E, '123') + }) + // referential + it('Should transform embedded module codec 2', () => { + const T = Type.Module({ + A: Type.Transform(Type.String()) + .Decode((value) => parseInt(value)) + .Encode((value) => value.toString()), + B: Type.Ref('A'), + }).Import('B') + const D = Value.Decode(T, '123') + const E = Value.Encode(T, 123) + Assert.IsEqual(D, 123) + Assert.IsEqual(E, '123') + }) + // deep-referential + it('Should transform embedded module codec 3', () => { + const T = Type.Module({ + A: Type.Transform(Type.String()) + .Decode((value) => parseInt(value)) + .Encode((value) => value.toString()), + B: Type.Ref('A'), + C: Type.Ref('B'), + D: Type.Ref('C'), + E: Type.Ref('D'), + }).Import('E') + const D = Value.Decode(T, '123') + const E = Value.Encode(T, 123) + Assert.IsEqual(D, 123) + Assert.IsEqual(E, '123') + }) + // interior-transform referential + it('Should transform embedded module codec 4', () => { + const T = Type.Module({ + A: Type.String(), + B: Type.Ref('A'), + C: Type.Ref('B'), + T: Type.Transform(Type.Ref('C')) + .Decode((value) => parseInt(value as string)) + .Encode((value) => value.toString()), + X: Type.Ref('T'), + Y: Type.Ref('X'), + Z: Type.Ref('Y') + }).Import('Z') + const D = Value.Decode(T, '123') + const E = Value.Encode(T, 123) + Assert.IsEqual(D, 123) + Assert.IsEqual(E, '123') + }) +}) diff --git a/test/runtime/value/transform/index.ts b/test/runtime/value/transform/index.ts new file mode 100644 index 000000000..ff005d5f5 --- /dev/null +++ b/test/runtime/value/transform/index.ts @@ -0,0 +1,33 @@ +import './_nested' +import './any' +import './array' +import './async-iterator' +import './bigint' +import './boolean' +import './constructor' +import './date' +import './enum' +import './function' +import './import' +import './integer' +import './intersect' +import './iterator' +import './literal' +import './never' +import './not' +import './null' +import './number' +import './object' +import './promise' +import './record' +import './recursive' +import './ref' +import './string' +import './symbol' +import './template-literal' +import './tuple' +import './undefined' +import './union' +import './unknown' +import './unsafe' +import './void' diff --git a/test/runtime/value/transform/integer.ts b/test/runtime/value/transform/integer.ts new file mode 100644 index 000000000..099439aff --- /dev/null +++ b/test/runtime/value/transform/integer.ts @@ -0,0 +1,41 @@ +import * as Encoder from './_encoder' +import { Assert } from '../../assert' +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' + +describe('value/transform/Integer', () => { + // -------------------------------------------------------- + // Identity + // -------------------------------------------------------- + const T0 = Type.Transform(Type.Integer()) + .Decode((value) => value) + .Encode((value) => value) + it('Should decode identity', () => { + const R = Encoder.Decode(T0, 42) + Assert.IsEqual(R, 42) + }) + it('Should encode identity', () => { + const R = Encoder.Encode(T0, 42) + Assert.IsEqual(R, 42) + }) + it('Should throw on identity decode', () => { + Assert.Throws(() => Encoder.Decode(T0, null)) + }) + // -------------------------------------------------------- + // Mapped + // -------------------------------------------------------- + const T1 = Type.Transform(Type.Integer()) + .Decode((value) => 1) + .Encode((value) => 2) + it('Should decode mapped', () => { + const R = Encoder.Decode(T1, 1) + Assert.IsEqual(R, 1) + }) + it('Should encode mapped', () => { + const R = Encoder.Encode(T1, null) + Assert.IsEqual(R, 2) + }) + it('Should throw on mapped decode', () => { + Assert.Throws(() => Encoder.Decode(T1, null)) + }) +}) diff --git a/test/runtime/value/transform/intersect.ts b/test/runtime/value/transform/intersect.ts new file mode 100644 index 000000000..7e0bcc825 --- /dev/null +++ b/test/runtime/value/transform/intersect.ts @@ -0,0 +1,180 @@ +import * as Encoder from './_encoder' +import { Assert } from '../../assert' +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' + +describe('value/transform/Intersect', () => { + // -------------------------------------------------------- + // Identity + // -------------------------------------------------------- + // prettier-ignore + const T0 = Type.Transform(Type.Intersect([ + Type.Object({ x: Type.Number() }), + Type.Object({ y: Type.Number() }) + ])) + .Decode(value => value) + .Encode(value => value) + it('Should decode identity', () => { + const R = Encoder.Decode(T0, { x: 1, y: 2 }) + Assert.IsEqual(R, { x: 1, y: 2 }) + }) + it('Should encode identity', () => { + const R = Encoder.Encode(T0, { x: 1, y: 2 }) + Assert.IsEqual(R, { x: 1, y: 2 }) + }) + it('Should throw on identity decode', () => { + Assert.Throws(() => Encoder.Decode(T0, null)) + }) + // -------------------------------------------------------- + // Mapped + // -------------------------------------------------------- + // prettier-ignore + const T1 = Type.Transform(Type.Intersect([ + Type.Object({ x: Type.Number() }), + Type.Object({ y: Type.Number() }) + ])) + .Decode((value) => 1) + .Encode((value) => ({ x: 1, y: 2 })) + it('Should decode mapped', () => { + const R = Encoder.Decode(T1, { x: 1, y: 2 }) + Assert.IsEqual(R, 1) + }) + it('Should encode mapped', () => { + const R = Encoder.Encode(T1, null) + Assert.IsEqual(R, { x: 1, y: 2 }) + }) + it('Should throw on mapped decode', () => { + Assert.Throws(() => Encoder.Decode(T1, null)) + }) + // -------------------------------------------------------- + // Mapped Property + // -------------------------------------------------------- + const N2 = Type.Transform(Type.Number()) + .Decode((value) => value.toString()) + .Encode((value) => parseFloat(value)) + // prettier-ignore + const T2 = Type.Transform(Type.Intersect([ + Type.Object({ x: Type.Number() }), + Type.Object({ y: N2 }) + ])) + .Decode((value) => value) + .Encode((value) => value) + it('Should decode property', () => { + const R = Encoder.Decode(T2, { x: 1, y: 2 }) + Assert.IsEqual(R, { x: 1, y: '2' }) + }) + it('Should encode property', () => { + const R = Encoder.Encode(T2, { x: 1, y: '2' }) + Assert.IsEqual(R, { x: 1, y: 2 }) + }) + it('Should throw on property decode', () => { + Assert.Throws(() => Encoder.Decode(T2, null)) + }) + // -------------------------------------------------------- + // Unevaluated Property + // -------------------------------------------------------- + const N3 = Type.Transform(Type.Number()) + .Decode((value) => value.toString()) + .Encode((value) => parseFloat(value)) + // prettier-ignore + const T3 = Type.Intersect([ + Type.Object({ x: Type.Number() }), + Type.Object({ y: Type.Number() }) + ], { + unevaluatedProperties: N3 + }) + it('Should decode unevaluated property', () => { + const R = Encoder.Decode(T3, { x: 1, y: 2, z: 3 }) + Assert.IsEqual(R, { x: 1, y: 2, z: '3' }) + }) + it('Should encode unevaluated property', () => { + const R = Encoder.Encode(T3, { x: 1, y: 2, z: '3' }) + Assert.IsEqual(R, { x: 1, y: 2, z: 3 }) + }) + it('Should throw on unevaluated property decode', () => { + Assert.Throws(() => Encoder.Decode(T3, null)) + }) + // -------------------------------------------------------- + // Transform Intersection Interior (Not Supported) + // -------------------------------------------------------- + it('Should throw on intersected interior transform types', () => { + const N4 = Type.Transform(Type.Number()) + .Decode((value) => value) + .Encode((value) => value) + Assert.Throws(() => Type.Intersect([N4, N4])) + }) + // -------------------------------------------------------- + // Transform Intersection Exterior + // -------------------------------------------------------- + const T4 = Type.Transform(Type.Intersect([Type.Number(), Type.Number()])) + .Decode((value) => value + 1) + .Encode((value) => value - 1) + it('Should decode exterior value type', () => { + const R = Encoder.Decode(T4, 1) + Assert.IsEqual(R, 2) + }) + it('Should encode exterior value type', () => { + const R = Encoder.Encode(T4, 2) + Assert.IsEqual(R, 1) + }) + it('Should throw on exterior value type decode', () => { + Assert.Throws(() => Encoder.Decode(T4, null)) + }) + // -------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/discussions/672 + // -------------------------------------------------------- + // prettier-ignore + { + const A = Type.Object({ isHybrid: Type.Boolean() }) + const T = Type.Transform(A) + .Decode((value) => ({ isHybrid: value.isHybrid ? 1 : 0 })) + .Encode((value) => ({ isHybrid: value.isHybrid === 1 ? true : false })) + const I = Type.Intersect([ + Type.Object({ model: Type.String() }), + Type.Object({ features: Type.Array(T) }), + ]) + it('Should decode nested 1', () => { + const value = Value.Decode(T, { isHybrid: true }) + Assert.IsEqual(value, { isHybrid: 1 }) + }) + // prettier-ignore + it('Should decode nested 2', () => { + const value = Value.Decode(I, { + model: 'Prius', + features: [ + { isHybrid: true }, + { isHybrid: false } + ], + }) + Assert.IsEqual(value, { + model: 'Prius', + features: [ + { isHybrid: 1 }, + { isHybrid: 0 } + ], + }) + }) + it('should encode nested 1', () => { + let value = Value.Encode(T, { isHybrid: 1 }) + Assert.IsEqual(value, { isHybrid: true }) + }) + // prettier-ignore + it('Should encode nested 2', () => { + const value = Value.Encode(I, { + model: 'Prius', + features: [ + { isHybrid: 1 }, + { isHybrid: 0 } + ], + }) + Assert.IsEqual(value, { + model: 'Prius', + features: [ + { isHybrid: true }, + { isHybrid: false } + + ], + }) + }) + } +}) diff --git a/test/runtime/value/transform/iterator.ts b/test/runtime/value/transform/iterator.ts new file mode 100644 index 000000000..66188acf1 --- /dev/null +++ b/test/runtime/value/transform/iterator.ts @@ -0,0 +1,41 @@ +import * as Encoder from './_encoder' +import { Assert } from '../../assert' +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' + +describe('value/transform/Iterator', () => { + // -------------------------------------------------------- + // Identity + // -------------------------------------------------------- + const T0 = Type.Transform(Type.Iterator(Type.Number())) + .Decode((value) => value) + .Encode((value) => value) + it('Should decode identity', () => { + const R = Encoder.Decode(T0, (function* (): any {})()) + Assert.IsTrue(Symbol.iterator in R) + }) + it('Should encode identity', () => { + const R = Encoder.Encode(T0, (function* (): any {})()) + Assert.IsTrue(Symbol.iterator in R) + }) + it('Should throw on identity decode', () => { + Assert.Throws(() => Encoder.Decode(T0, null)) + }) + // -------------------------------------------------------- + // Mapped + // -------------------------------------------------------- + const T1 = Type.Transform(Type.Iterator(Type.Number())) + .Decode((value) => 1) + .Encode((value) => (function* (): any {})()) + it('Should decode mapped', () => { + const R = Encoder.Decode(T1, (function* (): any {})()) + Assert.IsEqual(R, 1) + }) + it('Should encode mapped', () => { + const R = Encoder.Encode(T1, null) + Assert.IsTrue(Symbol.iterator in R) + }) + it('Should throw on mapped decode', () => { + Assert.Throws(() => Encoder.Decode(T1, null)) + }) +}) diff --git a/test/runtime/value/transform/literal.ts b/test/runtime/value/transform/literal.ts new file mode 100644 index 000000000..ef88502bb --- /dev/null +++ b/test/runtime/value/transform/literal.ts @@ -0,0 +1,72 @@ +import * as Encoder from './_encoder' +import { Assert } from '../../assert' +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' + +describe('value/transform/Literal', () => { + // ----------------------------------------------- + // Identity + // ----------------------------------------------- + const T0 = Type.Transform(Type.Literal(123)) + .Decode((value) => value) + .Encode((value) => value) + it('Should decode mapped', () => { + const R = Encoder.Decode(T0, 123) + Assert.IsEqual(R, 123) + }) + it('Should encode mapped', () => { + const R = Encoder.Encode(T0, 123) + Assert.IsEqual(R, 123) + }) + // ----------------------------------------------- + // LiteralString + // ----------------------------------------------- + const T1 = Type.Transform(Type.Literal('hello')) + .Decode((value) => 1) + .Encode((value) => 'hello' as const) + it('Should decode literal string', () => { + const R = Encoder.Decode(T1, 'hello') + Assert.IsEqual(R, 1) + }) + it('Should encode literal string', () => { + const R = Encoder.Encode(T1, null) + Assert.IsEqual(R, 'hello') + }) + it('Should throw on decode literal string', () => { + Assert.Throws(() => Encoder.Decode(T1, null)) + }) + // ----------------------------------------------- + // LiteralNumber + // ----------------------------------------------- + const T2 = Type.Transform(Type.Literal(2)) + .Decode((value) => 1) + .Encode((value) => 2 as const) + it('Should decode literal number', () => { + const R = Encoder.Decode(T2, 2) + Assert.IsEqual(R, 1) + }) + it('Should encode literal number', () => { + const R = Encoder.Encode(T2, null) + Assert.IsEqual(R, 2) + }) + it('Should throw on decode literal number', () => { + Assert.Throws(() => Encoder.Decode(T2, null)) + }) + // ----------------------------------------------- + // LiteralBoolean + // ----------------------------------------------- + const T3 = Type.Transform(Type.Literal(true)) + .Decode((value) => 1) + .Encode((value) => true as const) + it('Should decode literal boolean', () => { + const R = Encoder.Decode(T3, true) + Assert.IsEqual(R, 1) + }) + it('Should encode literal boolean', () => { + const R = Encoder.Encode(T3, null) + Assert.IsEqual(R, true) + }) + it('Should throw on decode literal boolean', () => { + Assert.Throws(() => Encoder.Decode(T3, null)) + }) +}) diff --git a/test/runtime/value/transform/never.ts b/test/runtime/value/transform/never.ts new file mode 100644 index 000000000..97e03478a --- /dev/null +++ b/test/runtime/value/transform/never.ts @@ -0,0 +1,34 @@ +import * as Encoder from './_encoder' +import { Assert } from '../../assert' +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' + +describe('value/transform/Never', () => { + // -------------------------------------------------------- + // Identity + // -------------------------------------------------------- + const T0 = Type.Transform(Type.Never()) + .Decode((value) => value) + // @ts-ignore + .Encode((value) => value) + it('Should throw on identity encode', () => { + Assert.Throws(() => Encoder.Encode(T0, undefined)) + }) + it('Should throw on identity decode', () => { + Assert.Throws(() => Encoder.Decode(T0, undefined)) + }) + // -------------------------------------------------------- + // Mapped + // -------------------------------------------------------- + const T1 = Type.Transform(Type.Never()) + // @ts-ignore + .Decode((value) => 1) + // @ts-ignore + .Encode((value) => 1) + it('Should throw on mapped encode', () => { + Assert.Throws(() => Encoder.Encode(T1, undefined)) + }) + it('Should throw on mapped decode', () => { + Assert.Throws(() => Encoder.Decode(T1, undefined)) + }) +}) diff --git a/test/runtime/value/transform/not.ts b/test/runtime/value/transform/not.ts new file mode 100644 index 000000000..68387ee58 --- /dev/null +++ b/test/runtime/value/transform/not.ts @@ -0,0 +1,41 @@ +import * as Encoder from './_encoder' +import { Assert } from '../../assert' +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' + +describe('value/transform/Not', () => { + // -------------------------------------------------------- + // Identity + // -------------------------------------------------------- + const T0 = Type.Transform(Type.Not(Type.String())) + .Decode((value) => value) + .Encode((value) => value) + it('Should decode identity', () => { + const R = Encoder.Decode(T0, 1) + Assert.IsEqual(R, 1) + }) + it('Should encode identity', () => { + const R = Encoder.Encode(T0, 1) + Assert.IsEqual(R, 1) + }) + it('Should throw on identity decode', () => { + Assert.Throws(() => Encoder.Decode(T0, '')) + }) + // -------------------------------------------------------- + // Mapped + // -------------------------------------------------------- + const T1 = Type.Transform(Type.Not(Type.String())) + .Decode((value) => 1) + .Encode((value) => 2) + it('Should decode mapped', () => { + const R = Encoder.Decode(T1, null) + Assert.IsEqual(R, 1) + }) + it('Should encode mapped', () => { + const R = Encoder.Encode(T1, null) + Assert.IsEqual(R, 2) + }) + it('Should throw on decode not', () => { + Assert.Throws(() => Encoder.Decode(T1, 'hello')) + }) +}) diff --git a/test/runtime/value/transform/null.ts b/test/runtime/value/transform/null.ts new file mode 100644 index 000000000..0cf90dc05 --- /dev/null +++ b/test/runtime/value/transform/null.ts @@ -0,0 +1,41 @@ +import * as Encoder from './_encoder' +import { Assert } from '../../assert' +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' + +describe('value/transform/Null', () => { + // -------------------------------------------------------- + // Identity + // -------------------------------------------------------- + const T0 = Type.Transform(Type.Null()) + .Decode((value) => value) + .Encode((value) => value) + it('Should decode identity', () => { + const R = Encoder.Decode(T0, null) + Assert.IsEqual(R, null) + }) + it('Should encode identity', () => { + const R = Encoder.Encode(T0, null) + Assert.IsEqual(R, null) + }) + it('Should throw on identity decode', () => { + Assert.Throws(() => Encoder.Decode(T0, undefined)) + }) + // -------------------------------------------------------- + // Mapped + // -------------------------------------------------------- + const T1 = Type.Transform(Type.Null()) + .Decode((value) => 1) + .Encode((value) => null) + it('Should decode mapped', () => { + const R = Encoder.Decode(T1, null) + Assert.IsEqual(R, 1) + }) + it('Should encode mapped', () => { + const R = Encoder.Encode(T1, 123) + Assert.IsEqual(R, null) + }) + it('Should throw on mapped decode', () => { + Assert.Throws(() => Encoder.Decode(T1, undefined)) + }) +}) diff --git a/test/runtime/value/transform/number.ts b/test/runtime/value/transform/number.ts new file mode 100644 index 000000000..2f06b50ff --- /dev/null +++ b/test/runtime/value/transform/number.ts @@ -0,0 +1,41 @@ +import * as Encoder from './_encoder' +import { Assert } from '../../assert' +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' + +describe('value/transform/Number', () => { + // -------------------------------------------------------- + // Identity + // -------------------------------------------------------- + const T0 = Type.Transform(Type.Number()) + .Decode((value) => value) + .Encode((value) => value) + it('Should decode identity', () => { + const R = Encoder.Decode(T0, 42) + Assert.IsEqual(R, 42) + }) + it('Should encode identity', () => { + const R = Encoder.Encode(T0, 42) + Assert.IsEqual(R, 42) + }) + it('Should throw on identity decode', () => { + Assert.Throws(() => Encoder.Decode(T0, null)) + }) + // -------------------------------------------------------- + // Mapped + // -------------------------------------------------------- + const T1 = Type.Transform(Type.Number()) + .Decode((value) => value + 1) + .Encode((value) => value - 1) + it('Should decode mapped', () => { + const R = Encoder.Decode(T1, 1) + Assert.IsEqual(R, 2) + }) + it('Should encode mapped', () => { + const R = Encoder.Encode(T1, 2) + Assert.IsEqual(R, 1) + }) + it('Should throw on mapped decode', () => { + Assert.Throws(() => Encoder.Decode(T1, undefined)) + }) +}) diff --git a/test/runtime/value/transform/object.ts b/test/runtime/value/transform/object.ts new file mode 100644 index 000000000..8580201ef --- /dev/null +++ b/test/runtime/value/transform/object.ts @@ -0,0 +1,302 @@ +import * as Encoder from './_encoder' +import { Assert } from '../../assert' + +import { TypeSystemPolicy } from '@sinclair/typebox/system' +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' + +// prettier-ignore +describe('value/transform/Object', () => { + // -------------------------------------------------------- + // Identity + // -------------------------------------------------------- + const T0 = Type.Transform( + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + ) + .Decode((value) => value) + .Encode((value) => value) + it('Should decode identity', () => { + const R = Encoder.Decode(T0, { x: 1, y: 2 }) + Assert.IsEqual(R, { x: 1, y: 2 }) + }) + it('Should encode identity', () => { + const R = Encoder.Encode(T0, { x: 1, y: 2 }) + Assert.IsEqual(R, { x: 1, y: 2 }) + }) + it('Should throw on identity decode', () => { + Assert.Throws(() => Encoder.Decode(T0, undefined)) + }) + // ---------------------------------------------------------- + // Object + // ---------------------------------------------------------- + const T1 = Type.Transform( + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + ) + .Decode((value) => 42) + .Encode((value) => ({ x: 1, y: 2 })) + it('Should decode mapped', () => { + const R = Encoder.Decode(T1, { x: 1, y: 2 }) + Assert.IsEqual(R, 42) + }) + it('Should encode mapped', () => { + const R = Encoder.Encode(T1, null) + Assert.IsEqual(R, { x: 1, y: 2 }) + }) + it('Should throw on mapped decode', () => { + Assert.Throws(() => Encoder.Decode(T1, undefined)) + }) + // ---------------------------------------------------------- + // Object: Transform Property + // ---------------------------------------------------------- + const N2 = Type.Transform(Type.Integer()) + .Decode((value) => value.toString()) + .Encode((value) => parseInt(value)) + const T2 = Type.Object({ + x: N2, + y: N2, + }) + it('Should decode transform property', () => { + const R = Encoder.Decode(T2, { x: 1, y: 2 }) + Assert.IsEqual(R, { x: '1', y: '2' }) + }) + it('Should encode transform property', () => { + const R = Encoder.Encode(T2, { x: '1', y: '2' }) + Assert.IsEqual(R, { x: 1, y: 2 }) + }) + it('Should throw on decode transform property', () => { + Assert.Throws(() => Encoder.Decode(T2, undefined)) + }) + // ---------------------------------------------------------- + // Object: Transform Property Nested (Twizzle) + // ---------------------------------------------------------- + const N3 = Type.Transform(Type.Integer()) + .Decode((value) => value.toString()) + .Encode((value) => parseInt(value)) + const T3 = Type.Transform( + Type.Object({ + x: N3, + y: N3, + }), + ) + .Decode((value) => ({ x: value.y, y: value.x })) + .Encode((value) => ({ x: value.y, y: value.x })) + it('Should decode transform property nested', () => { + const R = Encoder.Decode(T3, { x: 1, y: 2 }) + Assert.IsEqual(R, { x: '2', y: '1' }) + }) + it('Should encode transform property nested', () => { + const R = Encoder.Encode(T3, { x: '1', y: '2' }) + Assert.IsEqual(R, { x: 2, y: 1 }) + }) + it('Should throw on decode transform property nested', () => { + Assert.Throws(() => Encoder.Decode(T3, undefined)) + }) + // ---------------------------------------------------------- + // Object Additional Properties + // ---------------------------------------------------------- + const N4 = Type.Transform(Type.Integer()) + .Decode((value) => value.toString()) + .Encode((value) => parseInt(value)) + const T4 = Type.Transform( + Type.Object( + { + x: Type.Number(), + }, + { + additionalProperties: N4, + }, + ), + ) + .Decode((value) => value) + .Encode((value) => value) + it('Should decode additional property', () => { + const R = Encoder.Decode(T4, { x: 1, y: 2 }) + Assert.IsEqual(R, { x: 1, y: '2' }) + }) + it('Should encode additional property', () => { + const R = Encoder.Encode(T4, { x: 1, y: '5' }) + Assert.IsEqual(R, { x: 1, y: 5 }) + }) + it('Should throw on additional property 1', () => { + Assert.Throws(() => Encoder.Decode(T4, undefined)) + }) + it('Should throw on additional property 2', () => { + Assert.Throws(() => Encoder.Decode(T4, { x: 1, y: true })) + }) + // ------------------------------------------------------------ + // Map + // ------------------------------------------------------------ + const T5 = Type.Transform(Type.Object({ x: Type.String(), y: Type.String() })) + .Decode((value) => new Map(Object.entries(value))) + .Encode((value) => Object.fromEntries(value.entries()) as any) + it('should decode map', () => { + const R = Encoder.Decode(T5, { x: 'hello', y: 'world' }) + Assert.IsInstanceOf(R, Map) + Assert.IsEqual(R.get('x'), 'hello') + Assert.IsEqual(R.get('y'), 'world') + }) + it('should encode map', () => { + const R = Encoder.Encode( + T5, + new Map([ + ['x', 'hello'], + ['y', 'world'], + ]), + ) + Assert.IsEqual(R, { x: 'hello', y: 'world' }) + }) + it('Should throw on map decode', () => { + Assert.Throws(() => Encoder.Decode(T5, {})) + }) + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/859 + // ---------------------------------------------------------------- + it('Should decode for nested transform with renamed property', () => { + class User { constructor(public name: string, public createdAt: Date) { } } + const TDate = Type.Transform(Type.Number()) + .Decode(v => new Date(v)) + .Encode(v => v.getTime()) + const TUser = Type.Transform(Type.Object({ + name: Type.String(), + created_at: TDate + })) + .Decode(v => new User(v.name, v.created_at)) + .Encode(v => ({ name: v.name, created_at: v.createdAt })) + + const D = Value.Decode(TUser, { name: 'name', created_at: 0 }) + const E = Value.Encode(TUser, D) + + Assert.IsEqual(E.name, 'name') + Assert.IsEqual(E.created_at, 0) + }) + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/865 + // ---------------------------------------------------------------- + it('Should decode for null prototype', () => { + const N = Type.Transform(Type.Number()) + .Decode(value => value.toString()) + .Encode(value => parseInt(value)) + const T = Type.Object({ x: N }) + const A = Object.create(null); A.x = 1 + const B = Object.create(null); B.x = '1' + const D = Value.Decode(T, A) + const E = Value.Encode(T, B) + Assert.IsEqual(D, { x: '1' }) + Assert.IsEqual(E, { x: 1 }) + }) + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/958 + // ---------------------------------------------------------------- + it('Should not decode missing optional properties 0', () => { + let Invoked = false + const S = Type.Transform(Type.RegExp(/foo/)) + .Decode((value) => { Invoked = true; return value }) + .Encode((value) => value) + const T = Type.Object({ value: Type.Optional(S) }) + const D = Value.Decode(T, { value: 'foo' }) + Assert.IsEqual(D, { value: 'foo' }) + Assert.IsTrue(Invoked) + }) + it('Should not decode missing optional properties 1', () => { + let Invoked = false + const S = Type.Transform(Type.RegExp(/foo/)) + .Decode((value) => { Invoked = true; return value }) + .Encode((value) => value) + const T = Type.Object({ value: Type.Optional(S) }) + const D = Value.Decode(T, {}) + Assert.IsEqual(D, {}) + Assert.IsFalse(Invoked) + }) + it('Should not decode missing optional properties 2', () => { + let Invoked = false + const S = Type.Transform(Type.RegExp(/foo/)) + .Decode((value) => { Invoked = true; return value }) + .Encode((value) => value) + const T = Type.Object({ value: Type.Optional(S) }) + const D = Value.Decode(T, { value: undefined }) + Assert.IsEqual(D, { value: undefined }) + Assert.IsFalse(Invoked) + }) + it('Should not decode missing optional properties 3 (ExactOptionalPropertyTypes)', () => { + let [Invoked, Revert] = [false, TypeSystemPolicy.ExactOptionalPropertyTypes] + TypeSystemPolicy.ExactOptionalPropertyTypes = true + const S = Type.Transform(Type.RegExp(/foo/)) + .Decode((value) => { Invoked = true; return value }) + .Encode((value) => value) + const T = Type.Object({ value: Type.Optional(S) }) + const D = Value.Decode(T, {}) + Assert.IsEqual(D, {}) + Assert.IsFalse(Invoked) + TypeSystemPolicy.ExactOptionalPropertyTypes = Revert + }) + it('Should not decode missing optional properties 4 (ExactOptionalPropertyTypes)', () => { + let [Invoked, Revert] = [false, TypeSystemPolicy.ExactOptionalPropertyTypes] + TypeSystemPolicy.ExactOptionalPropertyTypes = true + const S = Type.Transform(Type.RegExp(/foo/)) + .Decode((value) => { Invoked = true; return value }) + .Encode((value) => value) + const T = Type.Object({ value: Type.Optional(S) }) + Assert.Throws(() => Value.Decode(T, { value: undefined })) + Assert.IsFalse(Invoked) + TypeSystemPolicy.ExactOptionalPropertyTypes = Revert + }) + it('Should not encode missing optional properties 0', () => { + let Invoked = false + const S = Type.Transform(Type.RegExp(/foo/)) + .Decode((value) => value) + .Encode((value) => { Invoked = true; return value }) + const T = Type.Object({ value: Type.Optional(S) }) + const D = Value.Encode(T, { value: 'foo' }) + Assert.IsEqual(D, { value: 'foo' }) + Assert.IsTrue(Invoked) + }) + it('Should not encode missing optional properties 1', () => { + let Invoked = false + const S = Type.Transform(Type.RegExp(/foo/)) + .Decode((value) => value) + .Encode((value) => { Invoked = true; return value }) + const T = Type.Object({ value: Type.Optional(S) }) + const D = Value.Encode(T, {}) + Assert.IsEqual(D, {}) + Assert.IsFalse(Invoked) + }) + it('Should not encode missing optional properties 2', () => { + let Invoked = false + const S = Type.Transform(Type.RegExp(/foo/)) + .Decode((value) => value) + .Encode((value) => { Invoked = true; return value }) + const T = Type.Object({ value: Type.Optional(S) }) + const D = Value.Encode(T, { value: undefined }) + Assert.IsEqual(D, { value: undefined }) + Assert.IsFalse(Invoked) + }) + it('Should not encode missing optional properties 3 (ExactOptionalPropertyTypes)', () => { + let [Invoked, Revert] = [false, TypeSystemPolicy.ExactOptionalPropertyTypes] + TypeSystemPolicy.ExactOptionalPropertyTypes = true + const S = Type.Transform(Type.RegExp(/foo/)) + .Decode((value) => value) + .Encode((value) => { Invoked = true; return value }) + const T = Type.Object({ value: Type.Optional(S) }) + const D = Value.Encode(T, {}) + Assert.IsEqual(D, {}) + Assert.IsFalse(Invoked) + TypeSystemPolicy.ExactOptionalPropertyTypes = Revert + }) + it('Should not encode missing optional properties 4 (ExactOptionalPropertyTypes)', () => { + let [Invoked, Revert] = [false, TypeSystemPolicy.ExactOptionalPropertyTypes] + TypeSystemPolicy.ExactOptionalPropertyTypes = true + const S = Type.Transform(Type.RegExp(/foo/)) + .Decode((value) => value) + .Encode((value) => { Invoked = true; return value }) + const T = Type.Object({ value: Type.Optional(S) }) + Assert.Throws(() => Value.Encode(T, { value: undefined })) + Assert.IsFalse(Invoked) + TypeSystemPolicy.ExactOptionalPropertyTypes = Revert + }) +}) diff --git a/test/runtime/value/transform/promise.ts b/test/runtime/value/transform/promise.ts new file mode 100644 index 000000000..991bf5822 --- /dev/null +++ b/test/runtime/value/transform/promise.ts @@ -0,0 +1,41 @@ +import * as Encoder from './_encoder' +import { Assert } from '../../assert' +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' + +describe('value/transform/Promise', () => { + // -------------------------------------------------------- + // Identity + // -------------------------------------------------------- + const T0 = Type.Transform(Type.Promise(Type.Number())) + .Decode((value) => value) + .Encode((value) => value) + it('Should decode identity', () => { + const R = Encoder.Decode(T0, Promise.resolve(1)) + Assert.IsTrue(R instanceof Promise) + }) + it('Should encode identity', () => { + const R = Encoder.Encode(T0, Promise.resolve(1)) + Assert.IsTrue(R instanceof Promise) + }) + it('Should throw on identity decode', () => { + Assert.Throws(() => Encoder.Decode(T0, undefined)) + }) + // -------------------------------------------------------- + // Mapped + // -------------------------------------------------------- + const T = Type.Transform(Type.Promise(Type.Number())) + .Decode((value) => 1) + .Encode((value) => Promise.resolve(1)) + it('Should decode mapped', () => { + const R = Encoder.Decode(T, Promise.resolve(1)) + Assert.IsEqual(R, 1) + }) + it('Should encode mapped', () => { + const R = Encoder.Encode(T, null) + Assert.IsTrue(R instanceof Promise) + }) + it('Should throw on mapped decode', () => { + Assert.Throws(() => Encoder.Decode(T, null)) + }) +}) diff --git a/test/runtime/value/transform/record.ts b/test/runtime/value/transform/record.ts new file mode 100644 index 000000000..6f53ef552 --- /dev/null +++ b/test/runtime/value/transform/record.ts @@ -0,0 +1,123 @@ +import * as Encoder from './_encoder' +import { Assert } from '../../assert' +import { Type } from '@sinclair/typebox' + +describe('value/transform/Record', () => { + // ------------------------------------------------------------ + // Identity + // ------------------------------------------------------------ + const T0 = Type.Transform(Type.Record(Type.String(), Type.Boolean())) + .Decode((value) => value) + .Encode((value) => value) + it('Should decode identity', () => { + const R = Encoder.Decode(T0, { + a: true, + b: false, + }) + Assert.IsEqual(R, { + a: true, + b: false, + }) + }) + it('Should encode identity', () => { + const R = Encoder.Encode(T0, { + a: true, + b: false, + }) + Assert.IsEqual(R, { + a: true, + b: false, + }) + }) + it('Should throw on identity', () => { + Assert.Throws(() => Encoder.Decode(T0, null)) + }) + // ------------------------------------------------------------ + // Additional Properties True + // ------------------------------------------------------------ + const N1 = Type.Transform(Type.Number()) + .Decode((value) => `number-${value.toString()}`) + .Encode((value) => parseFloat(value.replace(/number-/g, ''))) + const T1 = Type.Record(Type.Number(), N1) + it('Should decode additional properties allowed', () => { + const R = Encoder.Decode(T1, { + 0: 1, + a: true, + }) + Assert.IsEqual(R, { + 0: 'number-1', + a: true, + }) + }) + it('Should encode additional properties allowed', () => { + const R = Encoder.Encode(T1, { + 0: 'number-1', + a: true, + }) + Assert.IsEqual(R, { + 0: 1, + a: true, + }) + }) + it('Should throw on additional properties allowed', () => { + Assert.Throws(() => Encoder.Decode(T1, null)) + }) + // ------------------------------------------------------------ + // Complex Transform + // ------------------------------------------------------------ + const N2 = Type.Transform(Type.Number()) + .Decode((value) => `number-${value.toString()}`) + .Encode((value) => parseFloat(value.replace(/number-/g, ''))) + const B2 = Type.Transform(Type.Boolean()) + .Decode((value) => (value ? 'TRUE' : 'FALSE')) + .Encode((value) => (value === 'TRUE' ? true : false)) + const T3 = Type.Record(Type.Number(), N2, { additionalProperties: B2 }) + it('Should decode complex', () => { + const R = Encoder.Decode(T3, { + 0: 1, + a: true, + }) + Assert.IsEqual(R, { + 0: 'number-1', + a: 'TRUE', + }) + }) + it('Should encode complex', () => { + const R = Encoder.Encode(T3, { + 0: 'number-1', + a: 'TRUE', + }) + Assert.IsEqual(R, { + 0: 1, + a: true, + }) + }) + it('Should throw on complex decode', () => { + Assert.Throws(() => Encoder.Decode(T3, null)) + }) + // ------------------------------------------------------------ + // Map + // ------------------------------------------------------------ + const T4 = Type.Transform(Type.Record(Type.String(), Type.String())) + .Decode((value) => new Map(Object.entries(value))) + .Encode((value) => Object.fromEntries(value.entries())) + it('should decode map', () => { + const R = Encoder.Decode(T4, { x: 'hello', y: 'world' }) + Assert.IsInstanceOf(R, Map) + Assert.IsEqual(R.get('x'), 'hello') + Assert.IsEqual(R.get('y'), 'world') + }) + it('should encode map', () => { + const R = Encoder.Encode( + T4, + new Map([ + ['x', 'hello'], + ['y', 'world'], + ]), + ) + Assert.IsEqual(R, { x: 'hello', y: 'world' }) + }) + it('Should throw on map decode', () => { + Assert.Throws(() => Encoder.Decode(T4, null)) + }) +}) diff --git a/test/runtime/value/transform/recursive.ts b/test/runtime/value/transform/recursive.ts new file mode 100644 index 000000000..abea62630 --- /dev/null +++ b/test/runtime/value/transform/recursive.ts @@ -0,0 +1,132 @@ +import * as Encoder from './_encoder' +import { Assert } from '../../assert' +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' + +describe('value/transform/Recursive', () => { + // -------------------------------------------------------- + // Identity + // -------------------------------------------------------- + const T0 = Type.Transform( + Type.Recursive((This) => + Type.Object({ + value: Type.Number(), + nodes: Type.Array(This), + }), + ), + ) + .Decode((value) => value) + .Encode((value) => value) + it('Should decode identity', () => { + const R = Encoder.Decode(T0, { + value: 1, + nodes: [ + { value: 2, nodes: [] }, + { value: 3, nodes: [] }, + ], + }) + Assert.IsEqual(R, { + value: 1, + nodes: [ + { value: 2, nodes: [] }, + { value: 3, nodes: [] }, + ], + }) + }) + it('Should encode identity', () => { + const R = Encoder.Encode(T0, { + value: 1, + nodes: [ + { value: 2, nodes: [] }, + { value: 3, nodes: [] }, + ], + }) + Assert.IsEqual(R, { + value: 1, + nodes: [ + { value: 2, nodes: [] }, + { value: 3, nodes: [] }, + ], + }) + }) + it('Should throw on identity decode', () => { + Assert.Throws(() => Encoder.Decode(T0, undefined)) + }) + // ----------------------------------------------- + // Mapped + // ----------------------------------------------- + const T1 = Type.Transform( + Type.Recursive((This) => + Type.Object({ + value: Type.Number(), + nodes: Type.Array(This), + }), + ), + ) + .Decode((value) => 1) + .Encode((value) => ({ value: 1, nodes: [] })) + it('Should decode mapped', () => { + const R = Encoder.Decode(T1, { + value: 1, + nodes: [ + { value: 2, nodes: [] }, + { value: 3, nodes: [] }, + ], + }) + Assert.IsEqual(R, 1) + }) + it('Should encode mapped', () => { + const R = Encoder.Encode(T1, null) + Assert.IsEqual(R, { value: 1, nodes: [] }) + }) + it('Should throw on mapped decode', () => { + Assert.Throws(() => Encoder.Decode(T1, null)) + }) + // ----------------------------------------------- + // Recursive Property Remap + // ----------------------------------------------- + const N2 = Type.Transform(Type.Number()) + .Decode((value) => new Date(value)) + .Encode((value) => value.getTime()) + const T2 = Type.Recursive((This) => + Type.Object({ + value: N2, + nodes: Type.Array(This), + }), + ) + it('Should decode property', () => { + const R = Encoder.Decode(T2, { + value: 1, + nodes: [ + { value: 2, nodes: [] }, + { value: 3, nodes: [] }, + ], + }) + Assert.IsEqual(R, { + value: new Date(1), + nodes: [ + { value: new Date(2), nodes: [] }, + { value: new Date(3), nodes: [] }, + ], + }) + }) + it('Should encode property', () => { + const R = Encoder.Encode(T2, { + value: new Date(1), + nodes: [ + { value: new Date(2), nodes: [] }, + { value: new Date(3), nodes: [] }, + ], + }) + Assert.IsEqual(R, { + value: 1, + nodes: [ + { value: 2, nodes: [] }, + { value: 3, nodes: [] }, + ], + }) + }) + it('Should throw on decode property', () => { + Assert.Throws(() => Encoder.Decode(T2, null)) + }) +}) diff --git a/test/runtime/value/transform/ref.ts b/test/runtime/value/transform/ref.ts new file mode 100644 index 000000000..39113f41b --- /dev/null +++ b/test/runtime/value/transform/ref.ts @@ -0,0 +1,63 @@ +import * as Encoder from './_encoder' +import { Assert } from '../../assert' +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' + +describe('value/transform/Ref', () => { + // -------------------------------------------------------- + // Identity + // -------------------------------------------------------- + const N0 = Type.Number({ $id: 'N0' }) + const T0 = Type.Transform(Type.Ref('N0')) + .Decode((value) => value) + .Encode((value) => value) + it('Should decode mapped', () => { + const R = Encoder.Decode(T0, [N0], 0) + Assert.IsEqual(R, 0) + }) + it('Should encode mapped', () => { + const R = Encoder.Encode(T0, [N0], 0) + Assert.IsEqual(R, 0) + }) + it('Should throw on identity decode', () => { + Assert.Throws(() => Encoder.Decode(T0, null)) + }) + // -------------------------------------------------------- + // Mapped + // -------------------------------------------------------- + const N1 = Type.Number({ $id: 'N1' }) + const T1 = Type.Transform(Type.Unsafe(Type.Ref('N1'))) + .Decode((value) => value + 1) + .Encode((value) => value - 1) + it('Should decode mapped', () => { + const R = Encoder.Decode(T1, [N1], 0) + Assert.IsEqual(R, 1) + }) + it('Should encode mapped', () => { + const R = Encoder.Encode(T1, [N1], 1) + Assert.IsEqual(R, 0) + }) + it('Should throw on mapped decode', () => { + Assert.Throws(() => Encoder.Decode(T1, null)) + }) + // -------------------------------------------------------- + // Mapped Remote + // -------------------------------------------------------- + const N2 = Type.Transform(Type.Number({ $id: 'N2' })) + .Decode((value) => value + 1) + .Encode((value) => value - 1) + const T2 = Type.Transform(Type.Unsafe(Type.Ref('N2'))) + .Decode((value) => value + 1) + .Encode((value) => value - 1) + it('Should decode mapped remote', () => { + const R = Encoder.Decode(T2, [N2], 0) + Assert.IsEqual(R, 2) + }) + it('Should encode mapped remote', () => { + const R = Encoder.Encode(T2, [N2], 2) + Assert.IsEqual(R, 0) + }) + it('Should throw on mapped remote decode', () => { + Assert.Throws(() => Encoder.Decode(T1, null)) + }) +}) diff --git a/test/runtime/value/transform/string.ts b/test/runtime/value/transform/string.ts new file mode 100644 index 000000000..3419c8691 --- /dev/null +++ b/test/runtime/value/transform/string.ts @@ -0,0 +1,41 @@ +import * as Encoder from './_encoder' +import { Assert } from '../../assert' +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' + +describe('value/transform/String', () => { + // -------------------------------------------------------- + // Identity + // -------------------------------------------------------- + // const T0 = Type.Transform(Type.String()) + // .Decode((value) => value) + // .Encode((value) => value) + // it('Should decode identity', () => { + // const R = Encoder.Decode(T0, 'hello') + // Assert.IsEqual(R, 'hello') + // }) + // it('Should encode identity', () => { + // const R = Encoder.Encode(T0, 'hello') + // Assert.IsEqual(R, 'hello') + // }) + // it('Should throw on identity decode', () => { + // Assert.Throws(() => Encoder.Decode(T0, null)) + // }) + // -------------------------------------------------------- + // Mapped + // -------------------------------------------------------- + const T1 = Type.Transform(Type.String()) + .Decode((value) => value.split('').reverse().join('')) + .Encode((value) => value.split('').reverse().join('')) + it('Should decode mapped', () => { + const R = Encoder.Decode(T1, 'ABC') + Assert.IsEqual(R, 'CBA') + }) + it('Should encode mapped', () => { + const R = Encoder.Encode(T1, 'CBA') + Assert.IsEqual(R, 'ABC') + }) + it('Should throw on mapped decode', () => { + Assert.Throws(() => Encoder.Decode(T1, null)) + }) +}) diff --git a/test/runtime/value/transform/symbol.ts b/test/runtime/value/transform/symbol.ts new file mode 100644 index 000000000..a04a9d661 --- /dev/null +++ b/test/runtime/value/transform/symbol.ts @@ -0,0 +1,41 @@ +import * as Encoder from './_encoder' +import { Assert } from '../../assert' +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' + +describe('value/transform/String', () => { + // -------------------------------------------------------- + // Identity + // -------------------------------------------------------- + const T0 = Type.Transform(Type.Symbol()) + .Decode((value) => value) + .Encode((value) => value) + it('Should decode identity', () => { + const R = Encoder.Decode(T0, Symbol('hello')) + Assert.IsEqual(R.description, 'hello') + }) + it('Should encode identity', () => { + const R = Encoder.Encode(T0, Symbol('hello')) + Assert.IsEqual(R.description, 'hello') + }) + it('Should throw on identity decode', () => { + Assert.Throws(() => Encoder.Decode(T0, null)) + }) + // -------------------------------------------------------- + // Mapped + // -------------------------------------------------------- + const T1 = Type.Transform(Type.Symbol()) + .Decode((value) => Symbol(value.description?.split('').reverse().join(''))) + .Encode((value) => Symbol(value.description?.split('').reverse().join(''))) + it('Should decode mapped', () => { + const R = Encoder.Decode(T1, Symbol('ABC')) + Assert.IsEqual(R.description, 'CBA') + }) + it('Should encode mapped', () => { + const R = Encoder.Encode(T1, Symbol('CBA')) + Assert.IsEqual(R.description, 'ABC') + }) + it('Should throw on mapped decode', () => { + Assert.Throws(() => Encoder.Decode(T1, null)) + }) +}) diff --git a/test/runtime/value/transform/template-literal.ts b/test/runtime/value/transform/template-literal.ts new file mode 100644 index 000000000..3c8130a04 --- /dev/null +++ b/test/runtime/value/transform/template-literal.ts @@ -0,0 +1,41 @@ +import * as Encoder from './_encoder' +import { Assert } from '../../assert' +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' + +describe('value/transform/TemplateLiteral', () => { + // -------------------------------------------------------- + // Identity + // -------------------------------------------------------- + const T0 = Type.Transform(Type.TemplateLiteral([Type.Literal('hello')])) + .Decode((value) => value) + .Encode((value) => value) + it('Should decode identity', () => { + const R = Encoder.Decode(T0, 'hello') + Assert.IsEqual(R, 'hello') + }) + it('Should encode identity', () => { + const R = Encoder.Encode(T0, 'hello') + Assert.IsEqual(R, 'hello') + }) + it('Should throw on identity decode', () => { + Assert.Throws(() => Encoder.Decode(T0, null)) + }) + // -------------------------------------------------------- + // Mapped + // -------------------------------------------------------- + const T1 = Type.Transform(Type.TemplateLiteral([Type.Literal('ABC')])) + .Decode((value) => value.split('').reverse().join('') as 'CBA') + .Encode((value) => value.split('').reverse().join('') as 'ABC') + it('Should decode mapped', () => { + const R = Encoder.Decode(T1, 'ABC') + Assert.IsEqual(R, 'CBA') + }) + it('Should encode mapped', () => { + const R = Encoder.Encode(T1, 'CBA') + Assert.IsEqual(R, 'ABC') + }) + it('Should throw on mapped decode', () => { + Assert.Throws(() => Encoder.Decode(T1, null)) + }) +}) diff --git a/test/runtime/value/transform/tuple.ts b/test/runtime/value/transform/tuple.ts new file mode 100644 index 000000000..4b48377bc --- /dev/null +++ b/test/runtime/value/transform/tuple.ts @@ -0,0 +1,99 @@ +import * as Encoder from './_encoder' +import { Assert } from '../../assert' +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' + +describe('value/transform/Tuple', () => { + // -------------------------------------------------------- + // Identity + // -------------------------------------------------------- + const T0 = Type.Transform(Type.Tuple([Type.Number(), Type.Number()])) + .Decode((value) => value) + .Encode((value) => value) + it('Should decode identity', () => { + const R = Encoder.Decode(T0, [1, 2]) + Assert.IsEqual(R, [1, 2]) + }) + it('Should encode identity', () => { + const R = Encoder.Encode(T0, [1, 2]) + Assert.IsEqual(R, [1, 2]) + }) + it('Should throw on identity decode', () => { + Assert.Throws(() => Encoder.Decode(T0, null)) + }) + // -------------------------------------------------------- + // Mapped + // -------------------------------------------------------- + const T1 = Type.Transform(Type.Tuple([Type.Number()])) + .Decode((value) => [value[0] + 1] as [number]) + .Encode((value) => [value[0] - 1] as [number]) + it('Should decode mapped', () => { + const R = Encoder.Decode(T1, [0]) + Assert.IsEqual(R, [1]) + }) + it('Should encode mapped', () => { + const R = Encoder.Encode(T1, [1]) + Assert.IsEqual(R, [0]) + }) + it('Should throw on mapped decode', () => { + Assert.Throws(() => Encoder.Decode(T1, null)) + }) + // -------------------------------------------------------- + // Mapped Element + // -------------------------------------------------------- + const N2 = Type.Transform(Type.Number()) + .Decode((value) => value + 1) + .Encode((value) => value - 1) + const T2 = Type.Transform(Type.Tuple([N2])) + .Decode((value) => value) + .Encode((value) => value) + it('Should decode mapped element', () => { + const R = Encoder.Decode(T2, [0]) + Assert.IsEqual(R, [1]) + }) + it('Should encode mapped element', () => { + const R = Encoder.Encode(T2, [1]) + Assert.IsEqual(R, [0]) + }) + it('Should throw on mapped element decode', () => { + Assert.Throws(() => Encoder.Decode(T2, null)) + }) + // -------------------------------------------------------- + // Mapped Element + // -------------------------------------------------------- + const N3 = Type.Transform(Type.Number()) + .Decode((value) => value + 1) + .Encode((value) => value - 1) + const T3 = Type.Transform(Type.Tuple([N3])) + .Decode((value) => [value[0].toString()]) + .Encode((value) => [parseFloat(value[0])] as [number]) + it('Should decode mapped element', () => { + const R = Encoder.Decode(T3, [0]) + Assert.IsEqual(R, ['1']) + }) + it('Should encode mapped element', () => { + const R = Encoder.Encode(T3, ['1']) + Assert.IsEqual(R, [0]) + }) + it('Should throw on mapped element decode', () => { + Assert.Throws(() => Encoder.Decode(T3, null)) + }) + // ------------------------------------------------------------ + // Set + // ------------------------------------------------------------ + const T4 = Type.Transform(Type.Tuple([Type.Number()])) + .Decode((value) => new Set(value)) + .Encode((value) => [...value] as any) + it('should decode set', () => { + const R = Encoder.Decode(T4, [1]) + Assert.IsInstanceOf(R, Set) + Assert.IsTrue(R.has(1)) + }) + it('should encode set', () => { + const R = Encoder.Encode(T4, new Set([1])) + Assert.IsEqual(R, [1]) + }) + it('Should throw on set decode', () => { + Assert.Throws(() => Encoder.Decode(T4, {})) + }) +}) diff --git a/test/runtime/value/transform/undefined.ts b/test/runtime/value/transform/undefined.ts new file mode 100644 index 000000000..60c5a4c01 --- /dev/null +++ b/test/runtime/value/transform/undefined.ts @@ -0,0 +1,41 @@ +import * as Encoder from './_encoder' +import { Assert } from '../../assert' +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' + +describe('value/transform/Undefined', () => { + // -------------------------------------------------------- + // Identity + // -------------------------------------------------------- + const T0 = Type.Transform(Type.Undefined()) + .Decode((value) => value) + .Encode((value) => value) + it('Should decode identity', () => { + const R = Encoder.Decode(T0, undefined) + Assert.IsEqual(R, undefined) + }) + it('Should encode identity', () => { + const R = Encoder.Encode(T0, undefined) + Assert.IsEqual(R, undefined) + }) + it('Should throw on identity decode', () => { + Assert.Throws(() => Encoder.Decode(T0, null)) + }) + // -------------------------------------------------------- + // Mapped + // -------------------------------------------------------- + const T1 = Type.Transform(Type.Undefined()) + .Decode((value) => null) + .Encode((value) => undefined) + it('Should decode mapped', () => { + const R = Encoder.Decode(T1, undefined) + Assert.IsEqual(R, null) + }) + it('Should encode mapped', () => { + const R = Encoder.Encode(T1, null) + Assert.IsEqual(R, undefined) + }) + it('Should throw on mapped decode', () => { + Assert.Throws(() => Encoder.Decode(T1, null)) + }) +}) diff --git a/test/runtime/value/transform/union.ts b/test/runtime/value/transform/union.ts new file mode 100644 index 000000000..43152417f --- /dev/null +++ b/test/runtime/value/transform/union.ts @@ -0,0 +1,241 @@ +import * as Encoder from './_encoder' +import { Assert } from '../../assert' +import { Type } from '@sinclair/typebox' + +describe('value/transform/Union', () => { + // -------------------------------------------------------- + // Identity + // -------------------------------------------------------- + // prettier-ignore + const T0 = Type.Transform(Type.Union([ + Type.Object({ x: Type.Number() }), + Type.Object({ y: Type.Number() }) + ])) + .Decode((value) => value) + .Encode((value) => value) + it('Should decode identity', () => { + const R = Encoder.Decode(T0, { x: 1 }) + Assert.IsEqual(R, { x: 1 }) + }) + it('Should encode identity', () => { + const R = Encoder.Encode(T0, { y: 2 }) + Assert.IsEqual(R, { y: 2 }) + }) + it('Should throw on identity decode', () => { + Assert.Throws(() => Encoder.Decode(T0, null)) + }) + // -------------------------------------------------------- + // Mapped + // -------------------------------------------------------- + // prettier-ignore + const T1 = Type.Transform(Type.Union([ + Type.Object({ type: Type.Literal('hello') }), + Type.Object({ type: Type.Literal('world') }) + ])) + .Decode((value) => 'test' as const) + .Encode((value) => ({ type: 'hello' as const })) + it('Should decode mapped', () => { + const R = Encoder.Decode(T1, { type: 'hello' }) + Assert.IsEqual(R, 'test') + }) + it('Should encode mapped', () => { + const R = Encoder.Encode(T1, 'test') + Assert.IsEqual(R, { type: 'hello' }) + }) + it('Should throw on mapped decode', () => { + Assert.Throws(() => Encoder.Decode(T1, null)) + }) + // -------------------------------------------------------- + // Mapped ValueType + // -------------------------------------------------------- + const M21 = Type.Transform(Type.Number()) + .Decode((value) => value + 1) + .Encode((value) => value - 1) + const M22 = Type.Transform(M21) + .Decode((value) => value + 1) + .Encode((value) => value - 1) + // prettier-ignore + const T2 = Type.Transform(Type.Union([Type.String(), M22])) + .Decode((value) => value) + .Encode((value) => { + if (value === 'hello') return 'world' + return value + }) + it('Should decode value type 1', () => { + const R = Encoder.Decode(T2, 0) + Assert.IsEqual(R, 2) + }) + it('Should decode value type 2', () => { + const R = Encoder.Decode(T2, 'hello') + Assert.IsEqual(R, 'hello') + }) + it('Should encode value type 1', () => { + const R = Encoder.Encode(T2, 'hello') + Assert.IsEqual(R, 'world') + }) + it('Should encode value type 2', () => { + const R = Encoder.Encode(T2, 2) + Assert.IsEqual(R, 0) + }) + it('Should throw on value type decode', () => { + Assert.Throws(() => Encoder.Decode(T2, null)) + }) + // -------------------------------------------------------- + // Mapped ObjectType + // -------------------------------------------------------- + const N31 = Type.Transform( + Type.Object({ + x: Type.Number(), + }), + ) + .Decode((value) => ({ x: value.x + 1 })) + .Encode((value) => ({ x: value.x - 1 })) + const N32 = Type.Transform( + Type.Object({ + x: Type.String(), + }), + ) + .Decode((value) => ({ x: value.x.split('').reverse().join('') })) + .Encode((value) => ({ x: value.x.split('').reverse().join('') })) + // prettier-ignore + const T3 = Type.Transform(Type.Union([N31, N32])) + .Decode((value) => value) + .Encode((value) => value) + it('Should decode object types 1', () => { + const R = Encoder.Decode(T3, { x: 0 }) + Assert.IsEqual(R, { x: 1 }) + }) + it('Should decode object types 2', () => { + const R = Encoder.Decode(T3, { x: 'abc' }) + Assert.IsEqual(R, { x: 'cba' }) + }) + it('Should encode object types 1', () => { + const R = Encoder.Encode(T3, { x: 1 }) + Assert.IsEqual(R, { x: 0 }) + }) + it('Should encode object types 2', () => { + const R = Encoder.Encode(T3, { x: 'cba' }) + Assert.IsEqual(R, { x: 'abc' }) + }) + it('Should throw on object types decode', () => { + Assert.Throws(() => Encoder.Decode(T3, null)) + }) + // -------------------------------------------------------- + // Mapped Mixed Types + // -------------------------------------------------------- + const N41 = Type.Transform(Type.Number()) + .Decode((value) => value + 1) + .Encode((value) => value - 1) + // prettier-ignore + const N42 = Type.Transform(Type.Object({ + x: Type.Number() + })) + .Decode((value) => ({ x: value.x + 1 })) + .Encode((value) => ({ x: value.x - 1 })) + const N43 = Type.Transform(Type.Tuple([Type.Number()])) + .Decode((value) => [value[0] + 1]) + .Encode((value) => [value[0] - 1] as [number]) + // prettier-ignore + const T4 = Type.Transform(Type.Union([N41, N42, N43])) + .Decode((value) => typeof value === 'number' ? value + 1 : value) + .Encode((value) => typeof value === 'number' ? value - 1 : value) + it('Should decode mixed types 1', () => { + const R = Encoder.Decode(T4, { x: 0 }) + Assert.IsEqual(R, { x: 1 }) + }) + it('Should decode mixed types 2', () => { + const R = Encoder.Decode(T4, 0) + Assert.IsEqual(R, 2) + }) + it('Should decode mixed types 3', () => { + const R = Encoder.Decode(T4, [0]) + Assert.IsEqual(R, [1]) + }) + it('Should encode mixed types 1', () => { + const R = Encoder.Encode(T4, { x: 1 }) + Assert.IsEqual(R, { x: 0 }) + }) + it('Should encode mixed types 2', () => { + const R = Encoder.Encode(T4, 2) + Assert.IsEqual(R, 0) + }) + it('Should encode mixed types 3', () => { + const R = Encoder.Encode(T4, [1]) + Assert.IsEqual(R, [0]) + }) + it('Should throw on mixed types decode', () => { + Assert.Throws(() => Encoder.Decode(T4, null)) + }) + // -------------------------------------------------------- + // Interior Union Transform + // + // https://github.com/sinclairzx81/typebox/issues/631 + // -------------------------------------------------------- + const T51 = Type.Transform(Type.String()) + .Decode((value) => new Date(value)) + .Encode((value) => value.toISOString()) + const T52 = Type.Union([Type.Null(), T51]) + it('Should decode interior union 1', () => { + const R = Encoder.Decode(T52, null) + Assert.IsEqual(R, null) + }) + it('Should decode interior union 2', () => { + const R = Encoder.Decode(T52, new Date().toISOString()) + Assert.IsInstanceOf(R, Date) + }) + it('Should encode interior union 1', () => { + const R = Encoder.Encode(T52, null) + Assert.IsEqual(R, null) + }) + it('Should encode interior union 2', () => { + const D = new Date() + const R = Encoder.Encode(T52, D) + Assert.IsEqual(R, D.toISOString()) + }) + it('Should throw on interior union decode', () => { + Assert.Throws(() => Encoder.Decode(T52, {})) + }) + it('Should throw on interior union encode', () => { + Assert.Throws(() => Encoder.Encode(T52, 1)) + }) + // prettier-ignore + { // https://github.com/sinclairzx81/typebox/issues/676 + // interior-type + const S = Type.Transform(Type.String()) + .Decode((value: string) => new globalThis.Date(value)) + .Encode((value: Date) => value.toISOString()) + // union-type + const U = Type.Union([ + Type.Object({ date: S }), + Type.Number() + ]) + // expect date on decode + const T1 = Type.Transform(U) + .Decode((value) => { + Assert.IsTypeOf(value, 'object') + Assert.HasProperty(value, 'date') + Assert.IsInstanceOf(value.date, globalThis.Date); + return value + }) + .Encode((value) => value) + // expect number on decode + const T2 = Type.Transform(U) + .Decode((value) => { + Assert.IsTypeOf(value, 'number') + return value + }) + .Encode((value) => value) + + it('Should decode interior union 1', () => { + const R = Encoder.Decode(T1, { date: new globalThis.Date().toISOString() }) + Assert.IsTypeOf(R, 'object') + Assert.HasProperty(R, 'date') + Assert.IsInstanceOf(R.date, globalThis.Date); + }) + it('Should decode interior union 2', () => { + const R = Encoder.Decode(T2, 123) + Assert.IsTypeOf(R, 'number') + Assert.IsEqual(R, 123) + }) + } +}) diff --git a/test/runtime/value/transform/unknown.ts b/test/runtime/value/transform/unknown.ts new file mode 100644 index 000000000..463980d57 --- /dev/null +++ b/test/runtime/value/transform/unknown.ts @@ -0,0 +1,35 @@ +import * as Encoder from './_encoder' +import { Assert } from '../../assert' +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' + +describe('value/transform/Unknown', () => { + // -------------------------------------------------------- + // Identity + // -------------------------------------------------------- + const T0 = Type.Transform(Type.Unknown()) + .Decode((value) => value) + .Encode((value) => value) + it('Should decode mapped', () => { + const R = Encoder.Decode(T0, 123) + Assert.IsEqual(R, 123) + }) + it('Should encode mapped', () => { + const R = Encoder.Encode(T0, 123) + Assert.IsEqual(R, 123) + }) + // -------------------------------------------------------- + // Mapped + // -------------------------------------------------------- + const T1 = Type.Transform(Type.Unknown()) + .Decode((value) => 1) + .Encode((value) => 2) + it('Should decode mapped', () => { + const R = Encoder.Decode(T1, null) + Assert.IsEqual(R, 1) + }) + it('Should encode mapped', () => { + const R = Encoder.Encode(T1, null) + Assert.IsEqual(R, 2) + }) +}) diff --git a/test/runtime/value/transform/unsafe.ts b/test/runtime/value/transform/unsafe.ts new file mode 100644 index 000000000..f679e01c9 --- /dev/null +++ b/test/runtime/value/transform/unsafe.ts @@ -0,0 +1,47 @@ +import * as Encoder from './_encoder' +import { Assert } from '../../assert' +import { Value } from '@sinclair/typebox/value' +import { Type, Kind, TypeRegistry } from '@sinclair/typebox' + +describe('value/transform/Unsafe', () => { + // -------------------------------------------------------- + // Fixtures + // -------------------------------------------------------- + beforeEach(() => TypeRegistry.Set('Foo', (schema, value) => value !== null)) // throw on null + afterEach(() => TypeRegistry.Delete('Foo')) + const Foo = Type.Unsafe({ [Kind]: 'Foo' }) + // -------------------------------------------------------- + // Identity + // -------------------------------------------------------- + const T0 = Type.Transform(Foo) + .Decode((value) => value) + .Encode((value) => value) + it('Should decode identity', () => { + const R = Encoder.Decode(T0, 'hello') + Assert.IsEqual(R, 'hello') + }) + it('Should encode identity', () => { + const R = Encoder.Encode(T0, 'hello') + Assert.IsEqual(R, 'hello') + }) + it('Should throw on identity decode', () => { + Assert.Throws(() => Encoder.Decode(T0, null)) + }) + // -------------------------------------------------------- + // Mapped + // -------------------------------------------------------- + const T1 = Type.Transform(Foo) + .Decode((value) => value.split('').reverse().join('')) + .Encode((value) => value.split('').reverse().join('')) + it('Should decode mapped', () => { + const R = Encoder.Decode(T1, 'ABC') + Assert.IsEqual(R, 'CBA') + }) + it('Should encode mapped', () => { + const R = Encoder.Encode(T1, 'CBA') + Assert.IsEqual(R, 'ABC') + }) + it('Should throw on mapped decode', () => { + Assert.Throws(() => Encoder.Decode(T1, null)) + }) +}) diff --git a/test/runtime/value/transform/void.ts b/test/runtime/value/transform/void.ts new file mode 100644 index 000000000..16cd87248 --- /dev/null +++ b/test/runtime/value/transform/void.ts @@ -0,0 +1,40 @@ +import { Assert } from '../../assert' +import { Value } from '@sinclair/typebox/value' +import { Type } from '@sinclair/typebox' + +describe('value/transform/Void', () => { + // -------------------------------------------------------- + // Identity + // -------------------------------------------------------- + const T0 = Type.Transform(Type.Void()) + .Decode((value) => value) + .Encode((value) => value) + it('Should decode identity', () => { + const R = Value.Decode(T0, undefined) + Assert.IsEqual(R, undefined) + }) + it('Should encode identity', () => { + const R = Value.Encode(T0, undefined) + Assert.IsEqual(R, undefined) + }) + it('Should throw on identity decode', () => { + Assert.Throws(() => Value.Decode(T0, null)) + }) + // -------------------------------------------------------- + // Mapped + // -------------------------------------------------------- + const T1 = Type.Transform(Type.Void()) + .Decode((value) => null) + .Encode((value) => undefined) + it('Should decode mapped', () => { + const R = Value.Decode(T1, undefined) + Assert.IsEqual(R, null) + }) + it('Should encode mapped', () => { + const R = Value.Encode(T1, null) + Assert.IsEqual(R, undefined) + }) + it('Should throw on mapped decode', () => { + Assert.Throws(() => Value.Decode(T1, null)) + }) +}) diff --git a/test/static/any.ts b/test/static/any.ts new file mode 100644 index 000000000..90b417006 --- /dev/null +++ b/test/static/any.ts @@ -0,0 +1,4 @@ +import { Expect } from './assert' +import { Type } from '@sinclair/typebox' + +Expect(Type.Any()).ToStatic() diff --git a/test/static/argument.ts b/test/static/argument.ts new file mode 100644 index 000000000..588f80827 --- /dev/null +++ b/test/static/argument.ts @@ -0,0 +1,21 @@ +import { Expect } from './assert' +import { Type } from '@sinclair/typebox' + +const T = Type.Object({ + x: Type.Argument(0), + y: Type.Argument(1), + z: Type.Argument(2), +}) +const I = Type.Instantiate(T, [Type.Literal(1), Type.Literal(2), Type.Literal(3)]) +// Infer as Broadest Type (Pending Generic Constraints) +Expect(T).ToStatic<{ + x: unknown + y: unknown + z: unknown +}>() +// Infer as Narrowed Type +Expect(I).ToStatic<{ + x: 1 + y: 2 + z: 3 +}>() diff --git a/test/static/array.ts b/test/static/array.ts new file mode 100644 index 000000000..2ec60f3cc --- /dev/null +++ b/test/static/array.ts @@ -0,0 +1,24 @@ +import { Expect } from './assert' +import { Type } from '@sinclair/typebox' + +Expect(Type.Array(Type.String())).ToStatic() + +Expect( + Type.Array( + Type.Object({ + x: Type.Number(), + y: Type.Boolean(), + z: Type.String(), + }), + ), +).ToStatic< + { + x: number + y: boolean + z: string + }[] +>() + +Expect(Type.Array(Type.Array(Type.String()))).ToStatic() + +Expect(Type.Array(Type.Tuple([Type.String(), Type.Number()]))).ToStatic<[string, number][]>() diff --git a/test/static/assert.ts b/test/static/assert.ts new file mode 100644 index 000000000..862731dbf --- /dev/null +++ b/test/static/assert.ts @@ -0,0 +1,70 @@ +import { Static, StaticDecode, StaticEncode, TSchema } from '@sinclair/typebox' + +// ------------------------------------------------------------------ +// Symbols +// ------------------------------------------------------------------ +export declare const Unsatisfiable: unique symbol +// Warning: `never` and `any` satisfy the constraint `extends Expected<...>` +export type Expected<_> = { [Unsatisfiable]: never } +// ------------------------------------------------------------------ +// Gates +// ------------------------------------------------------------------ +export type If = T extends true ? Y : N +export type And = If +export type Or = If +export type Not = If +// ------------------------------------------------------------------ +// Helpers +// ------------------------------------------------------------------ +export type Extends = [T] extends [U] ? true : false +export type IsAny = 0 extends 1 & T ? true : false +export type IsNever = Extends +// ------------------------------------------------------------------ +// Constraints +// ------------------------------------------------------------------ +// See https://github.com/microsoft/TypeScript/issues/51011 +export type CircularHelper = [T] extends U ? T : Expected +// See https://github.com/Microsoft/TypeScript/issues/27024 +export type ConstrainEqual = (() => V extends T ? 1 : 2) extends () => V extends U ? 1 : 2 ? T : Expected +export type ConstraintMutuallyExtend = CircularHelper + +// Circular Error on TS 5.4.0 +// If U is never, there's nothing we can do +// export type ComplexConstraint = If< +// // If U is any, we can't use Expect or it would satisfy the constraint +// And>, IsAny>, +// never, +// If< +// Or< +// // If they are both any we are happy +// And, IsAny>, +// // If T extends U, but not because it's any, we are happy +// And, Not>> +// >, +// T, +// Expected +// > +// > +// ------------------------------------------------------------------ +// Expect +// ------------------------------------------------------------------ +export type ExpectResult = If< + IsNever>, + { ToStaticNever(): void }, + { + ToStatic, U>>(): void + ToStaticDecode, U>>(): void + ToStaticEncode, U>>(): void + // ToStatic>(): void + // ToStaticDecode>(): void + // ToStaticEncode>(): void + } +> +export function Expect(schema: T) { + return { + ToStatic() {}, + ToStaticNever() {}, + ToStaticDecode() {}, + ToStaticEncode() {}, + } as ExpectResult +} diff --git a/test/static/async-iterator.ts b/test/static/async-iterator.ts new file mode 100644 index 000000000..75364f1b0 --- /dev/null +++ b/test/static/async-iterator.ts @@ -0,0 +1,4 @@ +import { Expect } from './assert' +import { Type } from '@sinclair/typebox' + +Expect(Type.AsyncIterator(Type.String())).ToStatic>() diff --git a/test/static/awaited.ts b/test/static/awaited.ts new file mode 100644 index 000000000..e9c235c65 --- /dev/null +++ b/test/static/awaited.ts @@ -0,0 +1,20 @@ +import { Expect } from './assert' +import { Type } from '@sinclair/typebox' + +Expect(Type.Awaited(Type.String())).ToStatic() + +Expect(Type.Awaited(Type.Promise(Type.String()))).ToStatic() + +Expect(Type.Awaited(Type.Promise(Type.Promise(Type.String())))).ToStatic() + +// One Level + +Expect(Type.Awaited(Type.Union([Type.Promise(Type.String()), Type.Number()]))).ToStatic() + +Expect(Type.Awaited(Type.Intersect([Type.Promise(Type.Object({ a: Type.String() })), Type.Object({ b: Type.Number() })]))).ToStatic<{ a: string } & { b: number }>() + +// Two Levels + +Expect(Type.Awaited(Type.Union([Type.Promise(Type.Promise(Type.String())), Type.Number()]))).ToStatic() + +Expect(Type.Awaited(Type.Intersect([Type.Promise(Type.Promise(Type.Object({ a: Type.String() }))), Type.Object({ b: Type.Number() })]))).ToStatic<{ a: string } & { b: number }>() diff --git a/test/static/bigint.ts b/test/static/bigint.ts new file mode 100644 index 000000000..40c5b90e0 --- /dev/null +++ b/test/static/bigint.ts @@ -0,0 +1,4 @@ +import { Expect } from './assert' +import { Type } from '@sinclair/typebox' + +Expect(Type.BigInt()).ToStatic() diff --git a/test/static/boolean.ts b/test/static/boolean.ts new file mode 100644 index 000000000..9e1b01316 --- /dev/null +++ b/test/static/boolean.ts @@ -0,0 +1,4 @@ +import { Expect } from './assert' +import { Type } from '@sinclair/typebox' + +Expect(Type.Boolean()).ToStatic() diff --git a/test/static/capitalize.ts b/test/static/capitalize.ts new file mode 100644 index 000000000..b2982203e --- /dev/null +++ b/test/static/capitalize.ts @@ -0,0 +1,14 @@ +import { Expect } from './assert' +import { Type } from '@sinclair/typebox' + +Expect(Type.Capitalize(Type.Literal('hello'))).ToStatic<'Hello'>() + +Expect(Type.Capitalize(Type.Union([Type.Literal('hello'), Type.Literal('world')]))).ToStatic<'Hello' | 'World'>() + +Expect(Type.Capitalize(Type.TemplateLiteral('hello${0|1}'))).ToStatic<'Hello0' | 'Hello1'>() + +// prettier-ignore +Expect(Type.Capitalize(Type.TemplateLiteral([Type.Literal('hello'), Type.Union([Type.Literal(1), Type.Literal(2)])]))).ToStatic<'Hello1' | 'Hello2'>() + +// passthrough +Expect(Type.Capitalize(Type.Object({ x: Type.Number() }))).ToStatic<{ x: number }>() diff --git a/test/static/composite.ts b/test/static/composite.ts new file mode 100644 index 000000000..f124f6dbc --- /dev/null +++ b/test/static/composite.ts @@ -0,0 +1,213 @@ +import { Expect } from './assert' +import { Type, TOptional, TObject, TUnion, TIntersect, TNumber, TString, TBoolean } from '@sinclair/typebox' + +// ---------------------------------------------------------------------------- +// Overlapping - Non Varying +// ---------------------------------------------------------------------------- +{ + const A = Type.Object({ + A: Type.Number(), + }) + const B = Type.Object({ + A: Type.Number(), + }) + const T = Type.Composite([A, B]) + + Expect(T).ToStatic<{ + A: number + }>() +} +// ---------------------------------------------------------------------------- +// Overlapping - Varying +// ---------------------------------------------------------------------------- +{ + const A = Type.Object({ + A: Type.Number(), + }) + const B = Type.Object({ + A: Type.String(), + }) + const T = Type.Composite([A, B]) + + Expect(T).ToStatic<{ + A: never + }>() +} +// ---------------------------------------------------------------------------- +// Overlapping Single Optional +// ---------------------------------------------------------------------------- +{ + const A = Type.Object({ + A: Type.Optional(Type.Number()), + }) + const B = Type.Object({ + A: Type.Number(), + }) + const T = Type.Composite([A, B]) + + Expect(T).ToStatic<{ + A: number + }>() +} +// ---------------------------------------------------------------------------- +// Overlapping All Optional (Deferred) +// +// Note for: https://github.com/sinclairzx81/typebox/issues/419 +// ---------------------------------------------------------------------------- +{ + const A = Type.Object({ + A: Type.Optional(Type.Number()), + }) + const B = Type.Object({ + A: Type.Optional(Type.Number()), + }) + const T = Type.Composite([A, B]) + Expect(T).ToStatic<{ + A?: number | undefined + }>() +} +{ + const A = Type.Object({ + A: Type.Optional(Type.Number()), + }) + const B = Type.Object({ + A: Type.Number(), + }) + const T = Type.Composite([A, B]) + Expect(T).ToStatic<{ + A: number + }>() +} +{ + const A = Type.Object({ + A: Type.Number(), + }) + const B = Type.Object({ + A: Type.Number(), + }) + const T = Type.Composite([A, B]) + Expect(T).ToStatic<{ + A: number + }>() +} +// ---------------------------------------------------------------------------- +// Distinct Properties +// ---------------------------------------------------------------------------- +{ + const A = Type.Object({ + A: Type.Number(), + }) + const B = Type.Object({ + B: Type.Number(), + }) + const T = Type.Composite([A, B]) + + Expect(T).ToStatic<{ + A: number + B: number + }>() +} +// ---------------------------------------------------------------------------- +// Intersection Quirk +// +// TypeScript has an evaluation quirk for the following case where the first +// type evaluates the sub property as never, but the second evaluates the +// entire type as never. There is probably a reason for this behavior, but +// TypeBox supports the former evaluation. +// +// { x: number } & { x: string } -> { x: number } & { x: string } => { x: never } +// { x: number } & { x: boolean } -> never -> ... +// ---------------------------------------------------------------------------- +{ + // prettier-ignore + const T: TObject<{ + x: TIntersect<[TNumber, TBoolean]> + }> = Type.Composite([ + Type.Object({ x: Type.Number() }), + Type.Object({ x: Type.Boolean() }) + ]) +} +// ------------------------------------------------------------------ +// Intersect +// ------------------------------------------------------------------ +// prettier-ignore +{ + const T: TObject<{ + x: TNumber; + y: TNumber; + z: TNumber; + }> = Type.Composite([ + Type.Intersect([ + Type.Object({ x: Type.Number() }), + Type.Object({ y: Type.Number() }), + ]), + Type.Intersect([ + Type.Object({ z: Type.Number() }) + ]) + ]) +} +// prettier-ignore +{ + const T: TObject<{ + x: TIntersect<[TNumber, TNumber]>; + y: TIntersect<[TNumber, TNumber]>; + }> = Type.Composite([ + Type.Intersect([ + Type.Object({ x: Type.Number() }), + Type.Object({ y: Type.Number() }), + ]), + Type.Intersect([ + Type.Object({ x: Type.Number() }), + Type.Object({ y: Type.Number() }), + ]) + ]) +} +// prettier-ignore +{ + const T: TObject<{ + x: TIntersect<[TNumber, TNumber]>; + }> = Type.Composite([ + Type.Intersect([ + Type.Object({ x: Type.Optional(Type.Number()) }), + Type.Object({ x: Type.Number() }), + ]) + ]) +} +// prettier-ignore +{ + const T: TObject<{ + x: TOptional>; + }> = Type.Composite([ + Type.Intersect([ + Type.Object({ x: Type.Optional(Type.Number()) }), + Type.Object({ x: Type.Optional(Type.Number()) }), + ]) + ]) +} +// ------------------------------------------------------------------ +// Union +// ------------------------------------------------------------------ +// prettier-ignore +{ + const T: TObject<{ + x: TNumber; + }> = Type.Composite([ + Type.Union([ + Type.Object({ x: Type.Number() }), + Type.Object({ y: Type.Number() }) + ]), + Type.Object({ x: Type.Number() }) + ]) +} +// prettier-ignore +{ + const T: TObject<{ + x: TIntersect<[TUnion<[TString, TString]>, TNumber]>; + }> = Type.Composite([ + Type.Union([ + Type.Object({ x: Type.String() }), + Type.Object({ x: Type.String() }) + ]), + Type.Object({ x: Type.Number() }) + ]) +} diff --git a/test/static/const.ts b/test/static/const.ts new file mode 100644 index 000000000..d81d0fed7 --- /dev/null +++ b/test/static/const.ts @@ -0,0 +1,41 @@ +import { Expect } from './assert' +import { Type } from '@sinclair/typebox' + +// ------------------------------------------------------------------ +// Identity Types +// ------------------------------------------------------------------ +// prettier-ignore +Expect(Type.Const(undefined)).ToStatic() +// prettier-ignore +Expect(Type.Const(null)).ToStatic() +// prettier-ignore +Expect(Type.Const(Symbol())).ToStatic() +// prettier-ignore +Expect(Type.Const(1 as const)).ToStatic<1>() +// prettier-ignore +Expect(Type.Const('hello' as const)).ToStatic<'hello'>() +// prettier-ignore +Expect(Type.Const(true as const)).ToStatic() + +// ------------------------------------------------------------------ +// Complex Types +// ------------------------------------------------------------------ +// prettier-ignore +Expect(Type.Const(100n)).ToStatic() +// prettier-ignore +Expect(Type.Const(new Date())).ToStatic() +// prettier-ignore +Expect(Type.Const(new Uint8Array())).ToStatic() +// prettier-ignore +Expect(Type.Const(function () {})).ToStatic<() => unknown>() +// prettier-ignore +Expect(Type.Const((function *(): any {})())).ToStatic() +// prettier-ignore +Expect(Type.Const((async function *(): any {})())).ToStatic() +// todo: remove when dropping TS 4.0 +// prettier-ignore +Expect(Type.Const({ x: 1, y: { z: 2 } })).ToStatic<{ readonly x: number, readonly y: { readonly z: number }}>() +// prettier-ignore +Expect(Type.Const({ x: 1, y: { z: 2 } } as const)).ToStatic<{ readonly x: 1, readonly y: { readonly z: 2 }}>() +// prettier-ignore +Expect(Type.Const([1, 2, 3] as const)).ToStatic<[1, 2, 3]>() diff --git a/test/static/constructor-parameters.ts b/test/static/constructor-parameters.ts new file mode 100644 index 000000000..43db97852 --- /dev/null +++ b/test/static/constructor-parameters.ts @@ -0,0 +1,13 @@ +import { Expect } from './assert' +import { Type } from '@sinclair/typebox' + +const C = Type.Constructor( + [Type.Number(), Type.String()], + Type.Object({ + method: Type.Function([Type.Number(), Type.String()], Type.Boolean()), + }), +) + +const P = Type.ConstructorParameters(C) + +Expect(P).ToStatic<[number, string]>() diff --git a/test/static/constructor.ts b/test/static/constructor.ts new file mode 100644 index 000000000..59e435c62 --- /dev/null +++ b/test/static/constructor.ts @@ -0,0 +1,51 @@ +import { Expect } from './assert' +import { Type } from '@sinclair/typebox' +{ + // simple + const T = Type.Constructor([Type.Number(), Type.Boolean()], Type.String()) + Expect(T).ToStatic string>() +} +{ + // nested + // prettier-ignore + const T = Type.Constructor([Type.Number(), Type.String()], Type.Object({ + method: Type.Constructor([Type.Number(), Type.String()], Type.Boolean()), + })) + Expect(T).ToStatic { method: new (param_0: number, param_1: string) => boolean }>() +} +{ + // readonly-optional + const T = Type.Constructor([Type.ReadonlyOptional(Type.Array(Type.Number()))], Type.Number()) + Expect(T).ToStaticDecode number>() +} +{ + // readonly + const T = Type.Constructor([Type.Readonly(Type.Array(Type.Number()))], Type.Number()) + Expect(T).ToStaticDecode number>() +} +{ + // optional 1 + const T = Type.Constructor([Type.Optional(Type.Number())], Type.Number()) + Expect(T).ToStaticDecode number>() +} +{ + // optional 2 + const T = Type.Constructor([Type.Number(), Type.Optional(Type.Number())], Type.Number()) + Expect(T).ToStaticDecode number>() +} +{ + // decode 2 + const S = Type.Transform(Type.Integer()) + .Decode((value) => new Date(value)) + .Encode((value) => value.getTime()) + const T = Type.Constructor([S], Type.String()) + Expect(T).ToStaticDecode string>() +} +{ + // decode 1 + const S = Type.Transform(Type.Integer()) + .Decode((value) => new Date(value)) + .Encode((value) => value.getTime()) + const T = Type.Constructor([Type.Number()], S) + Expect(T).ToStaticDecode Date>() +} diff --git a/test/static/date.ts b/test/static/date.ts new file mode 100644 index 000000000..fe20220d5 --- /dev/null +++ b/test/static/date.ts @@ -0,0 +1,4 @@ +import { Expect } from './assert' +import { Type } from '@sinclair/typebox' + +Expect(Type.Date()).ToStatic() diff --git a/test/static/enum.ts b/test/static/enum.ts new file mode 100644 index 000000000..9818acfe1 --- /dev/null +++ b/test/static/enum.ts @@ -0,0 +1,42 @@ +import { Expect } from './assert' +import { Type } from '@sinclair/typebox' + +{ + // expect all variants + enum E { + A, + B = 'hello', + C = 42, + } + const T = Type.Enum(E) + Expect(T).ToStatic() +} +{ + // expect all variants + const T = Type.Enum({ + A: 1, + B: 2, + C: 3, + }) + Expect(T).ToStatic<1 | 2 | 3>() +} +{ + // expect variant overlap to reduce + const T = Type.Enum({ + A: 1, + B: 2, + C: 2, // overlap + }) + Expect(T).ToStatic<1 | 2>() +} +{ + // expect empty enum to be string (as empty enums T[keyof T] evaluates as string) + enum E {} + const T = Type.Enum(E) + Expect(T).ToStatic() +} +{ + // expect empty enum to be never + const T = Type.Enum({}) + Expect(T).ToStaticNever() +} diff --git a/test/static/exclude.ts b/test/static/exclude.ts new file mode 100644 index 000000000..a63c6c2f4 --- /dev/null +++ b/test/static/exclude.ts @@ -0,0 +1,99 @@ +import { Type, TLiteral, TUnion } from '@sinclair/typebox' +import { Expect } from './assert' + +{ + const T = Type.Exclude(Type.String(), Type.String()) + Expect(T).ToStaticNever() +} +{ + const T = Type.Exclude(Type.String(), Type.Number()) + Expect(T).ToStatic() +} +{ + const T = Type.Exclude(Type.Union([Type.Number(), Type.String(), Type.Boolean()]), Type.Number()) + Expect(T).ToStatic() +} +// ------------------------------------------------------------------------ +// TemplateLiteral | TemplateLiteral +// ------------------------------------------------------------------------ +{ + const A = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + const B = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + + const T = Type.Exclude(A, B) + Expect(T).ToStaticNever() +} +{ + const A = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + const B = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B')])]) + + const T = Type.Exclude(A, B) + Expect(T).ToStatic<'C'>() +} +{ + const A = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + const B = Type.TemplateLiteral([Type.Union([Type.Literal('A')])]) + const T = Type.Exclude(A, B) + Expect(T).ToStatic<'C' | 'B'>() +} +// ------------------------------------------------------------------------ +// TemplateLiteral | Union +// ------------------------------------------------------------------------ +{ + const A = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + const B = Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')]) + + const T = Type.Exclude(A, B) + Expect(T).ToStaticNever() +} +{ + const A = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + const B = Type.Union([Type.Literal('A'), Type.Literal('B')]) + + const T = Type.Exclude(A, B) + Expect(T).ToStatic<'C'>() +} +{ + const A = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + const B = Type.Union([Type.Literal('A')]) + const T = Type.Exclude(A, B) + Expect(T).ToStatic<'C' | 'B'>() +} +// ------------------------------------------------------------------------ +// Union | TemplateLiteral +// ------------------------------------------------------------------------ +{ + const A = Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')]) + const B = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + + const T = Type.Exclude(A, B) + Expect(T).ToStaticNever() +} +{ + const A = Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')]) + const B = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B')])]) + + const T = Type.Exclude(A, B) + Expect(T).ToStatic<'C'>() +} +{ + const A = Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')]) + const B = Type.TemplateLiteral([Type.Union([Type.Literal('A')])]) + const T = Type.Exclude(A, B) + Expect(T).ToStatic<'C' | 'B'>() +} +// https://github.com/sinclairzx81/typebox/issues/737 +{ + const U = Type.Union([Type.Literal('A'), Type.Literal('B')]) + const T = Type.Object({ + type: Type.Exclude(U, Type.Literal('A')), + }) + Expect(T).ToStatic<{ type: 'B' }>() +} +{ + const U = Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')]) + const T = Type.Object({ + type: Type.Exclude(U, Type.Literal('A')), + }) + Expect(T).ToStatic<{ type: 'B' | 'C' }>() +} diff --git a/test/static/extract.ts b/test/static/extract.ts new file mode 100644 index 000000000..b9e621e95 --- /dev/null +++ b/test/static/extract.ts @@ -0,0 +1,100 @@ +import { Type, TLiteral, TUnion } from '@sinclair/typebox' +import { Expect } from './assert' + +{ + const T = Type.Extract(Type.String(), Type.String()) + Expect(T).ToStatic() +} +{ + const T = Type.Extract(Type.String(), Type.Number()) + Expect(T).ToStaticNever() +} +{ + const T = Type.Extract(Type.Union([Type.Number(), Type.String(), Type.Boolean()]), Type.Number()) + Expect(T).ToStatic() +} +// ------------------------------------------------------------------------ +// TemplateLiteral | TemplateLiteral +// ------------------------------------------------------------------------ +{ + const A = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + const B = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + + const T = Type.Extract(A, B) + Expect(T).ToStatic<'A' | 'B' | 'C'>() +} +{ + const A = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + const B = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B')])]) + + const T = Type.Extract(A, B) + Expect(T).ToStatic<'A' | 'B'>() +} +{ + const A = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + const B = Type.TemplateLiteral([Type.Union([Type.Literal('A')])]) + const T = Type.Extract(A, B) + Expect(T).ToStatic<'A'>() +} +// ------------------------------------------------------------------------ +// TemplateLiteral | Union +// ------------------------------------------------------------------------ +{ + const A = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + const B = Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')]) + const T = Type.Extract(A, B) + Expect(T).ToStatic<'A' | 'B' | 'C'>() +} +{ + const A = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + const B = Type.Union([Type.Literal('A'), Type.Literal('B')]) + + const T = Type.Extract(A, B) + Expect(T).ToStatic<'A' | 'B'>() +} +{ + const A = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + const B = Type.Union([Type.Literal('A')]) + const T = Type.Extract(A, B) + Expect(T).ToStatic<'A'>() +} +// ------------------------------------------------------------------------ +// Union | TemplateLiteral +// ------------------------------------------------------------------------ +{ + const A = Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')]) + const B = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')])]) + + const T = Type.Extract(A, B) + Expect(T).ToStatic<'A' | 'B' | 'C'>() +} +{ + const A = Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')]) + const B = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B')])]) + + const T = Type.Extract(A, B) + Expect(T).ToStatic<'A' | 'B'>() +} +{ + const A = Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')]) + const B = Type.TemplateLiteral([Type.Union([Type.Literal('A')])]) + const T = Type.Extract(A, B) + Expect(T).ToStatic<'A'>() +} +// ------------------------------------------------------------------------ +// Nested (Inference Test) +// ------------------------------------------------------------------------ +{ + const U = Type.Union([Type.Literal('A'), Type.Literal('B')]) + const T = Type.Object({ + type: Type.Extract(U, Type.Literal('A')), + }) + Expect(T).ToStatic<{ type: 'A' }>() +} +{ + const U = Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')]) + const T = Type.Object({ + type: Type.Extract(U, Type.Union([Type.Literal('A'), Type.Literal('B')])), + }) + Expect(T).ToStatic<{ type: 'A' | 'B' }>() +} diff --git a/test/static/function.ts b/test/static/function.ts new file mode 100644 index 000000000..1ae3404aa --- /dev/null +++ b/test/static/function.ts @@ -0,0 +1,54 @@ +import { Expect } from './assert' +import { Type } from '@sinclair/typebox' + +{ + // simple + const T = Type.Function([Type.Number(), Type.Boolean()], Type.String()) + Expect(T).ToStatic<(param_0: number, param_1: boolean) => string>() +} +{ + // nested + // prettier-ignore + const T = Type.Function([Type.Number(), Type.String()], Type.Object({ + method: Type.Function([Type.Number(), Type.String()], Type.Boolean()), + })) + Expect(T).ToStatic<(param_0: number, param_1: string) => { method: (param_0: number, param_1: string) => boolean }>() +} +{ + // readonly-optional + const T = Type.Function([Type.ReadonlyOptional(Type.Array(Type.Number()))], Type.Number()) + Expect(T).ToStaticDecode<(param_0?: readonly number[]) => number>() +} +{ + // readonly + const T = Type.Function([Type.Readonly(Type.Array(Type.Number()))], Type.Number()) + Expect(T).ToStaticDecode<(param_0: readonly number[]) => number>() +} +{ + // optional 1 + const T = Type.Function([Type.Optional(Type.Number())], Type.Number()) + Expect(T).ToStaticDecode<(param_0?: number) => number>() +} +{ + // optional 2 + const T = Type.Function([Type.Number(), Type.Optional(Type.Number())], Type.Number()) + Expect(T).ToStaticDecode<(param_0: number, param_1?: number) => number>() +} +const F = Type.Constructor([Type.Readonly(Type.Array(Type.String()))], Type.Number()) + +{ + // decode 2 + const S = Type.Transform(Type.Integer()) + .Decode((value) => new Date(value)) + .Encode((value) => value.getTime()) + const T = Type.Function([S], Type.String()) + Expect(T).ToStaticDecode<(param_0: Date) => string>() +} +{ + // decode 1 + const S = Type.Transform(Type.Integer()) + .Decode((value) => new Date(value)) + .Encode((value) => value.getTime()) + const T = Type.Function([Type.Number()], S) + Expect(T).ToStaticDecode<(param_0: number) => Date>() +} diff --git a/test/static/import.ts b/test/static/import.ts new file mode 100644 index 000000000..dec8e8ec2 --- /dev/null +++ b/test/static/import.ts @@ -0,0 +1,155 @@ +import { Expect } from './assert' +import { Type, Static } from '@sinclair/typebox' + +// ------------------------------------------------------------------ +// Enum 1 +// ------------------------------------------------------------------ +{ + enum Enum { + A, + B, + } + + const T = Type.Module({ + T: Type.Object({ + value: Type.Enum(Enum), + }), + }).Import('T') + + Expect(T).ToStatic<{ + value: Enum + }>() +} + +// ------------------------------------------------------------------ +// Enum 2 +// ------------------------------------------------------------------ +{ + const T = Type.Module({ + T: Type.Object({ + value: Type.Enum({ + x: 1, + y: 2, + }), + }), + }).Import('T') + + Expect(T).ToStatic<{ + value: 1 | 2 + }>() +} +// ------------------------------------------------------------------ +// Record 1 +// ------------------------------------------------------------------ +// prettier-ignore +{ + const T = Type.Module({ + R: Type.Object({ x: Type.Number(), y: Type.Number() }), + T: Type.Record(Type.String(), Type.Ref('R')), + }).Import('T') + + type T = Static + Expect(T).ToStatic<{ + [key: string]: { x: number, y: number } + }>() +} +// ------------------------------------------------------------------ +// Record 2 +// ------------------------------------------------------------------ +// prettier-ignore +{ + const T = Type.Module({ + R: Type.Object({ x: Type.Number(), y: Type.Number() }), + T: Type.Record(Type.String(), Type.Partial(Type.Ref('R'))), + }).Import('T') + + type T = Static + Expect(T).ToStatic<{ + [key: string]: { x?: number, y?: number } + }>() +} +// ------------------------------------------------------------------ +// Modifiers 1 +// ------------------------------------------------------------------ +// prettier-ignore +{ + const Module = Type.Module({ + T: Type.Object({ + x: Type.ReadonlyOptional(Type.Null()), + y: Type.Readonly(Type.Null()), + z: Type.Optional(Type.Null()), + w: Type.Null() + }) + }) + const T = Module.Import('T') + type T = Static + Expect(T).ToStatic<{ + readonly x?: null, + readonly y: null, + z?: null, + w: null + }>() +} +// ------------------------------------------------------------------ +// Modifiers 2 +// ------------------------------------------------------------------ +// prettier-ignore +{ + const Module = Type.Module({ + T: Type.Object({ + x: Type.ReadonlyOptional(Type.Array(Type.Null())), + y: Type.Readonly(Type.Array(Type.Null())), + z: Type.Optional(Type.Array(Type.Null())), + w: Type.Array(Type.Null()) + }) + }) + const T = Module.Import('T') + type T = Static + Expect(T).ToStatic<{ + readonly x?: null[], + readonly y: null[], + z?:null[], + w: null[] + }>() +} +// ------------------------------------------------------------------ +// Modifiers 3 +// ------------------------------------------------------------------ +// prettier-ignore +{ + const Module = Type.Module({ + T: Type.Object({ + x: Type.Array(Type.Null()) + }), + // Computed Partial + U: Type.Partial(Type.Ref('T')) + }) + const T = Module.Import('U') + type T = Static + Expect(T).ToStatic<{ + x?: null[], + }>() +} +// ------------------------------------------------------------------ +// Ref inside Recursive +// ------------------------------------------------------------------ +// prettier-ignore +{ + const Module = Type.Module({ + T: Type.Recursive((_) => + Type.Object({ + M: Type.Ref("U"), + }) + ), + U: Type.Union([ + Type.Literal("A"), + Type.Literal("B") + ]), + }); + + const T = Module.Import("T"); + type T = Static; + Expect(T).ToStatic<{ + M: 'A'|'B' + }>(); +} diff --git a/test/static/index.ts b/test/static/index.ts new file mode 100644 index 000000000..2a27fadee --- /dev/null +++ b/test/static/index.ts @@ -0,0 +1,58 @@ +import './any' +import './argument' +import './array' +import './async-iterator' +import './awaited' +import './bigint' +import './boolean' +import './capitalize' +import './composite' +import './const' +import './constructor-parameters' +import './constructor' +import './date' +import './deref' +import './enum' +import './extract' +import './exclude' +import './function' +import './import' +import './indexed' +import './instance-type' +import './intersect' +import './iterator' +import './keyof' +import './literal' +import './lowercase' +import './mapped' +import './modifier' +import './namespace' +import './never' +import './not' +import './null' +import './number' +import './object' +import './omit' +import './optional' +import './parameters' +import './partial' +import './pick' +import './readonly-optional' +import './readonly' +import './recursive' +import './record' +import './ref' +import './regexp' +import './required' +import './rest' +import './return-type' +import './string' +import './symbol' +import './syntax' +import './template-literal' +import './transform' +import './tuple' +import './uncapitalize' +import './union' +import './unknown' +import './uppercase' diff --git a/test/static/indexed.ts b/test/static/indexed.ts new file mode 100644 index 000000000..fe5875b7b --- /dev/null +++ b/test/static/indexed.ts @@ -0,0 +1,276 @@ +import { Expect } from './assert' +import { Type, Static } from '@sinclair/typebox' + +{ + const T = Type.Object({ + x: Type.Number(), + y: Type.String(), + }) + const R = Type.Index(T, ['x', 'y']) + type O = Static + Expect(R).ToStatic() +} +{ + const T = Type.Tuple([Type.Number(), Type.String(), Type.Boolean()]) + const R = Type.Index(T, Type.Union([Type.Literal('0'), Type.Literal('1')])) + type O = Static + Expect(R).ToStatic() +} +{ + const T = Type.Tuple([Type.Number(), Type.String(), Type.Boolean()]) + const R = Type.Index(T, Type.Union([Type.Literal(0), Type.Literal(1)])) + type O = Static + Expect(R).ToStatic() +} +{ + const T = Type.Object({ + ab: Type.Number(), + ac: Type.String(), + }) + + const R = Type.Index(T, Type.TemplateLiteral([Type.Literal('a'), Type.Union([Type.Literal('b'), Type.Literal('c')])])) + type O = Static + Expect(R).ToStatic() +} +{ + const A = Type.Tuple([Type.String(), Type.Boolean()]) + const R = Type.Index(A, Type.Number()) + type O = Static + Expect(R).ToStatic() +} +{ + const A = Type.Tuple([Type.String()]) + const R = Type.Index(A, Type.Number()) + type O = Static + Expect(R).ToStatic() +} +{ + const A = Type.Tuple([]) + const R = Type.Index(A, Type.Number()) + type O = Static + Expect(R).ToStaticNever() +} +{ + const A = Type.Object({}) + const R = Type.Index(A, Type.BigInt()) // Support Overload + type O = Static + Expect(R).ToStaticNever() +} +{ + const A = Type.Array(Type.Number()) + const R = Type.Index(A, Type.BigInt()) // Support Overload + type O = Static + Expect(R).ToStaticNever() +} +// ------------------------------------------------------------------ +// Intersections +// ------------------------------------------------------------------ +{ + type A = { x: string; y: 1 } + type B = { x: string; y: number } + type C = A & B + type R = C['y'] + + const A = Type.Object({ x: Type.String(), y: Type.Literal(1) }) + const B = Type.Object({ x: Type.String(), y: Type.Number() }) + const C = Type.Intersect([A, B]) + const R = Type.Index(C, ['y']) + type O = Static + Expect(R).ToStatic<1>() +} +{ + type A = { x: string; y: 1 } + type B = { x: string; y: number } + type C = A & B + type R = C['x'] + + const A = Type.Object({ x: Type.String(), y: Type.Literal(1) }) + const B = Type.Object({ x: Type.String(), y: Type.Number() }) + const C = Type.Intersect([A, B]) + const R = Type.Index(C, ['x']) + type O = Static + Expect(R).ToStatic() +} +{ + type A = { x: string; y: 1 } + type B = { x: string; y: number } + type C = A & B + type R = C['x' | 'y'] + + const A = Type.Object({ x: Type.String(), y: Type.Literal(1) }) + const B = Type.Object({ x: Type.String(), y: Type.Number() }) + const C = Type.Intersect([A, B]) + const R = Type.Index(C, ['x', 'y']) + type O = Static + Expect(R).ToStatic() +} +{ + type A = { x: string; y: number } + type B = { x: number; y: number } + type C = A & B + type R = C['x'] + + const A = Type.Object({ x: Type.String(), y: Type.Number() }) + const B = Type.Object({ x: Type.Number(), y: Type.Number() }) + const C = Type.Intersect([A, B]) + const R = Type.Index(C, ['x']) + type O = Static + Expect(R).ToStaticNever() +} +{ + type A = { x: string; y: number } + type B = { x: number; y: number } + type C = A & B + type R = C['y'] + + const A = Type.Object({ x: Type.String(), y: Type.Number() }) + const B = Type.Object({ x: Type.Number(), y: Type.Number() }) + const C = Type.Intersect([A, B]) + const R = Type.Index(C, ['y']) + type O = Static + Expect(R).ToStatic() +} +{ + type A = { x: string; y: number } + type B = { x: number; y: number } + type C = A & B + type R = C['x' | 'y'] + + const A = Type.Object({ x: Type.String(), y: Type.Number() }) + const B = Type.Object({ x: Type.Number(), y: Type.Number() }) + const C = Type.Intersect([A, B]) + const R = Type.Index(C, ['x', 'y']) + type O = Static + Expect(R).ToStatic() +} +{ + type A = { x: string; y: 1 } + type B = { x: string; y: number } + type C = { x: string; y: number } + type D = { x: string } + type I = (A & B) & (C & D) + type R = I['x' | 'y'] + + const A = Type.Object({ x: Type.String(), y: Type.Literal(1) }) + const B = Type.Object({ x: Type.String(), y: Type.Number() }) + const C = Type.Object({ x: Type.String(), y: Type.Number() }) + const D = Type.Object({ x: Type.String() }) + const I = Type.Intersect([Type.Intersect([A, B]), Type.Intersect([C, D])]) + const R = Type.Index(I, ['x', 'y']) + type O = Static + Expect(R).ToStatic() +} +{ + type A = { x: string; y: 1 } + type B = { x: number; y: number } + type C = { x: string; y: number } + type D = { x: string } + type I = (A & B) & (C & D) + type R = I['x' | 'y'] + + const A = Type.Object({ x: Type.String(), y: Type.Literal(1) }) + const B = Type.Object({ x: Type.Number(), y: Type.Number() }) + const C = Type.Object({ x: Type.String(), y: Type.Number() }) + const D = Type.Object({ x: Type.String() }) + const I = Type.Intersect([Type.Intersect([A, B]), Type.Intersect([C, D])]) + const R = Type.Index(I, ['x', 'y']) + + // TUnion<[TIntersect<[TIntersect<[TString, TNumber]>, TIntersect<[TString, TString]>]>, TIntersect<[TIntersect<[TLiteral<...>, TNumber]>, TNumber]>]> + // TUnion<[TUnion<[TString, TNumber, TString, TString]>, TUnion<[TLiteral<1>, TNumber, TNumber]>]> + type O = Static + Expect(R).ToStatic<1>() +} +{ + type A = { x: string; y: 1 } + type B = { x: number; y: number } + type C = { x: string; y: number } + type D = { x: string } + type I = (A | B) & (C & D) + type R = I['x' | 'y'] + + const A = Type.Object({ x: Type.String(), y: Type.Literal(1) }) + const B = Type.Object({ x: Type.Number(), y: Type.Number() }) + const C = Type.Object({ x: Type.String(), y: Type.Number() }) + const D = Type.Object({ x: Type.String() }) + const I = Type.Intersect([Type.Union([A, B]), Type.Intersect([C, D])]) + const R = Type.Index(I, ['x', 'y']) + + // TUnion<[TIntersect<[TUnion<[TString, TNumber]>, TIntersect<[TString, TIntersect<[TString, TIntersect<[]>]>, TIntersect<[]>]>]>, TIntersect<...>]> + // TUnion<[TIntersect<[TUnion<[TString, TNumber]>, TIntersect<[TString, TString]>]>, TIntersect<[TUnion<[TLiteral<1>, TNumber]>, TNumber]>]> + // TUnion<[TIntersect<[TUnion<[TString, TNumber]>, TString, TString]>, TIntersect<[TUnion<[TLiteral<1>, TNumber]>, TNumber]>]> + type O = Static + Expect(R).ToStatic() +} +{ + type A = { x: 'A'; y: 1 } + type B = { x: 'B'; y: number } + type C = { x: 'C'; y: number } + type D = { x: 'D' } + type I = A | B | C | D + type R = I['x'] + + const A = Type.Object({ x: Type.Literal('A'), y: Type.Literal(1) }) + const B = Type.Object({ x: Type.Literal('B'), y: Type.Number() }) + const C = Type.Object({ x: Type.Literal('C'), y: Type.Number() }) + const D = Type.Object({ x: Type.Literal('D') }) + const I = Type.Union([A, B, C, D]) + const R = Type.Index(I, ['x']) + type O = Static + Expect(R).ToStatic<'A' | 'B' | 'C' | 'D'>() +} +{ + type I = { + x: string + y: number + z: I + } + type R = I['x' | 'y' | 'z'] + const I = Type.Recursive((This) => + Type.Object({ + x: Type.String(), + y: Type.Number(), + z: This, + }), + ) + const R = Type.Index(I, ['x', 'y', 'z']) // z unresolvable + type O = Static + Expect(R).ToStatic() +} +// ------------------------------------------------ +// Numeric | String Variants +// ------------------------------------------------ +{ + const T = Type.Object({ + 0: Type.Number(), + '1': Type.String(), + }) + const R = Type.Index(T, [0, '1']) + type O = Static + Expect(R).ToStatic() +} +{ + const T = Type.Object({ + '0': Type.Number(), + '1': Type.String(), + }) + const R = Type.Index(T, Type.KeyOf(T)) + type O = Static + Expect(R).ToStatic() +} +{ + const P = Type.Tuple([Type.Number(), Type.String()]) + const R = Type.Object({ + x: Type.Index(P, [0, 1]), + }) + Expect(R).ToStatic<{ x: number | string }>() +} +{ + const T = Type.Array(Type.String()) + const I = Type.Index(T, Type.Number()) + Expect(I).ToStatic() +} +{ + const T = Type.Array(Type.String()) + const I = Type.Index(T, ['[number]']) + Expect(I).ToStatic() +} diff --git a/test/static/intersect.ts b/test/static/intersect.ts new file mode 100644 index 000000000..f4fbffbf0 --- /dev/null +++ b/test/static/intersect.ts @@ -0,0 +1,53 @@ +import { Expect } from './assert' +import { Type, Static } from '@sinclair/typebox' + +{ + const A = Type.Object({ + A: Type.String(), + B: Type.String(), + }) + const B = Type.Object({ + X: Type.Number(), + Y: Type.Number(), + }) + const T = Type.Intersect([A, B]) + + Expect(T).ToStatic< + { + A: string + B: string + } & { + X: number + Y: number + } + >() +} + +{ + const A = Type.Object({ + A: Type.Optional(Type.String()), + }) + const B = Type.Object({ + B: Type.String(), + }) + const T = Type.Intersect([A, B]) + + Expect(T).ToStatic<{ A?: string | undefined } & { B: string }>() +} + +// https://github.com/sinclairzx81/typebox/issues/113 +// https://github.com/sinclairzx81/typebox/issues/187 +{ + const A = Type.Object({ A: Type.String() }) + const B = Type.Object({ B: Type.String() }) + const C = Type.Object({ C: Type.String() }) + const T = Type.Intersect([A, Type.Union([B, C])]) + type T = Static + const _0: T = { A: '', B: '' } + const _1: T = { A: '', C: '' } + const _3: T = { A: '', B: '', C: '' } + // invert equivelence (expect true both cases) + type T1 = T extends { A: string } & ({ B: string } | { C: string }) ? true : false + type T2 = { A: string } & ({ B: string } | { C: string }) extends T ? true : false + Expect(T).ToStatic<{ A: string } & ({ B: string } | { C: string })>() // solved! +} diff --git a/test/static/iterator.ts b/test/static/iterator.ts new file mode 100644 index 000000000..5727f641f --- /dev/null +++ b/test/static/iterator.ts @@ -0,0 +1,4 @@ +import { Expect } from './assert' +import { Type } from '@sinclair/typebox' + +Expect(Type.Iterator(Type.String())).ToStatic>() diff --git a/test/static/keyof.ts b/test/static/keyof.ts new file mode 100644 index 000000000..10d21df36 --- /dev/null +++ b/test/static/keyof.ts @@ -0,0 +1,102 @@ +import { Expect } from './assert' +import { Type } from '@sinclair/typebox' + +{ + const K = Type.KeyOf( + Type.Object({ + A: Type.Null(), + B: Type.Null(), + C: Type.Null(), + }), + ) + Expect(K).ToStatic<'A' | 'B' | 'C'>() +} + +{ + const T = Type.Pick( + Type.Object({ + A: Type.Null(), + B: Type.Null(), + C: Type.Null(), + }), + ['A', 'B'], + ) + + const K = Type.KeyOf(T) + + Expect(K).ToStatic<'A' | 'B'>() +} + +{ + const T = Type.Omit( + Type.Object({ + A: Type.Null(), + B: Type.Null(), + C: Type.Null(), + }), + ['A', 'B'], + ) + + const K = Type.KeyOf(T) + + Expect(K).ToStatic<'C'>() +} + +{ + const T = Type.KeyOf( + Type.Omit( + Type.Object({ + A: Type.Null(), + B: Type.Null(), + C: Type.Null(), + }), + ['A', 'B'], + ), + ) + Expect(T).ToStatic<'C'>() +} +{ + { + const A = Type.Object({ type: Type.Literal('A') }) + const B = Type.Object({ type: Type.Literal('B') }) + const C = Type.Object({ type: Type.Literal('C') }) + const Union = Type.Union([A, B, C]) + const Extended = Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }) + const T = Type.Intersect([Union, Extended]) + + const K1 = Type.KeyOf(T) + + Expect(K1).ToStatic<'type' | 'x' | 'y' | 'z'>() + + const P = Type.Omit(T, ['type', 'x']) + + const K2 = Type.KeyOf(P) + + Expect(K2).ToStatic<'y' | 'z'>() + } +} +{ + const T = Type.Recursive((Self) => + Type.Object({ + a: Type.String(), + b: Type.String(), + c: Type.String(), + d: Type.Array(Self), + }), + ) + const K = Type.KeyOf(T) + Expect(K).ToStatic<'a' | 'b' | 'c' | 'd'>() +} +{ + const T = Type.Object({ + a: Type.Optional(Type.String()), + b: Type.Optional(Type.String()), + c: Type.Optional(Type.String()), + }) + const K = Type.KeyOf(T) + Expect(K).ToStatic<'a' | 'b' | 'c'>() +} diff --git a/test/static/literal.ts b/test/static/literal.ts new file mode 100644 index 000000000..8a76ffd4f --- /dev/null +++ b/test/static/literal.ts @@ -0,0 +1,8 @@ +import { Expect } from './assert' +import { Type } from '@sinclair/typebox' + +Expect(Type.Literal('hello')).ToStatic<'hello'>() + +Expect(Type.Literal(true)).ToStatic() + +Expect(Type.Literal(42)).ToStatic<42>() diff --git a/test/static/lowercase.ts b/test/static/lowercase.ts new file mode 100644 index 000000000..f04ce5ece --- /dev/null +++ b/test/static/lowercase.ts @@ -0,0 +1,14 @@ +import { Expect } from './assert' +import { Type } from '@sinclair/typebox' + +Expect(Type.Lowercase(Type.Literal('HELLO'))).ToStatic<'hello'>() + +Expect(Type.Lowercase(Type.Union([Type.Literal('HELLO'), Type.Literal('WORLD')]))).ToStatic<'hello' | 'world'>() + +Expect(Type.Lowercase(Type.TemplateLiteral('HELLO${0|1}'))).ToStatic<'hello0' | 'hello1'>() + +// prettier-ignore +Expect(Type.Lowercase(Type.TemplateLiteral([Type.Literal('HELLO'), Type.Union([Type.Literal(1), Type.Literal(2)])]))).ToStatic<'hello1' | 'hello2'>() + +// passthrough +Expect(Type.Lowercase(Type.Object({ x: Type.Number() }))).ToStatic<{ x: number }>() diff --git a/test/static/mapped.ts b/test/static/mapped.ts new file mode 100644 index 000000000..1b57668b9 --- /dev/null +++ b/test/static/mapped.ts @@ -0,0 +1,417 @@ +import { Expect } from './assert' +import { Static, Type } from '@sinclair/typebox' + +// prettier-ignore +{ // Generative + const A = Type.Mapped(Type.Union([ + Type.Literal('x'), + Type.Literal('y'), + Type.Literal('z'), + ]), K => Type.Number()) + Expect(A).ToStatic<{ + x: number, + y: number, + z: number + }> + const B = Type.Mapped(Type.TemplateLiteral('${0|1}${0|1}'), K => Type.Number()) + Expect(B).ToStatic<{ + '00': number, + '01': number, + '10': number, + '11': number, + }> +} +// prettier-ignore +{ // Generative Nested +const T = Type.Mapped(Type.TemplateLiteral('${a|b}'), X => + Type.Mapped(Type.TemplateLiteral('${c|d}'), Y => + Type.Mapped(Type.TemplateLiteral('${e|f}'), Z => + Type.Tuple([X, Y, Z]) + ) + ) +) +type E = { + [X in `${'a' | 'b'}`]: { + [Y in `${'c' | 'd'}`]: { + [Z in `${'e' | 'f'}`]: [X, Y, Z] + } + } +} +Expect(T).ToStatic // ok +} +// prettier-ignore +{ // Identity + const T = Type.Object({ + x: Type.Number(), + y: Type.String(), + z: Type.Boolean() + }) + + const A = Type.Mapped(Type.KeyOf(T), K => K) + Expect(A).ToStatic<{ + x: 'x', + y: 'y', + z: 'z' + }>() + + const B = Type.Mapped(Type.KeyOf(T), K => Type.Index(T, K)) + Expect(B).ToStatic<{ + x: number, + y: string, + z: boolean + }>() +} +// prettier-ignore +{ // Extract + const T = Type.Object({ + x: Type.Union([Type.String(), Type.Number(), Type.Boolean()]) + }) + const A = Type.Mapped(Type.KeyOf(T), K => { + return Type.Extract(Type.Index(T, K), Type.String()) + }) + Expect(A).ToStatic<{ + x: string + }> + const B = Type.Mapped(Type.KeyOf(T), K => { + return Type.Extract(Type.Index(T, K), Type.Union([ + Type.String(), + Type.Number() + ])) + }) + Expect(B).ToStatic<{ + x: string | number + }> + const C = Type.Mapped(Type.KeyOf(T), K => { + return Type.Extract(Type.Index(T, K), Type.Null()) + }) + Expect(C).ToStatic<{ + x: never + }> +} +// prettier-ignore +{ // Numeric Keys + const T = Type.Object({ + 0: Type.Number(), + 1: Type.Number(), + 2: Type.Number() + }) + const A = Type.Mapped(Type.KeyOf(T), K => Type.Index(T, K)) + Expect(A).ToStatic<{ + 0: number, + 1: number, + 2: number + }> +} +// prettier-ignore +{ // Extends + const T = Type.Object({ + x: Type.Number(), + y: Type.String(), + z: Type.Boolean() + }) + const A = Type.Mapped(Type.KeyOf(T), K => { + return ( + Type.Extends(K, Type.Literal('x'), Type.Literal(1), + Type.Extends(K, Type.Literal('y'), Type.Literal(2), + Type.Extends(K, Type.Literal('z'), Type.Literal(3), Type.Never()))) + ) + }) + Expect(A).ToStatic<{ + x: 1, + y: 2, + z: 3 + }> + const B = Type.Mapped(Type.KeyOf(T), K => { + return ( + Type.Extends(Type.Index(T, K), Type.Number(), Type.Literal(3), + Type.Extends(Type.Index(T, K), Type.String(), Type.Literal(2), + Type.Extends(Type.Index(T, K), Type.Boolean(), Type.Literal(1), Type.Never()))) + ) + }) + Expect(B).ToStatic<{ + x: 3, + y: 2, + z: 1 + }> +} +// prettier-ignore +{ // Exclude + const T = Type.Object({ + x: Type.Union([Type.String(), Type.Number(), Type.Boolean()]) + }) + const A = Type.Mapped(Type.KeyOf(T), K => { + return Type.Exclude(Type.Index(T, K), Type.String()) + }) + Expect(A).ToStatic<{ + x: number | boolean + }> + const B = Type.Mapped(Type.KeyOf(T), K => { + return Type.Exclude(Type.Index(T, K), Type.Union([ + Type.String(), + Type.Number() + ])) + }) + Expect(B).ToStatic<{ + x: boolean + }> + const C = Type.Mapped(Type.KeyOf(T), K => { + return Type.Exclude(Type.Index(T, K), Type.Null()) + }) + Expect(C).ToStatic<{ + x: string | number | boolean + }> +} +// prettier-ignore +{ // Non-Evaluated Indexed + const T = Type.Object({ + x: Type.Number() + }) + const A = Type.Mapped(Type.KeyOf(T), K => Type.Array(Type.Index(T, K))) + Expect(A).ToStatic<{ x: number[] }> + + const B = Type.Mapped(Type.KeyOf(T), K => Type.Promise(Type.Index(T, K))) + Expect(B).ToStatic<{ x: Promise }> + + const C = Type.Mapped(Type.KeyOf(T), K => Type.Function([Type.Index(T, K)], Type.Index(T, K))) + Expect(C).ToStatic<{ x: (x: number) => number }> + + const D = Type.Mapped(Type.KeyOf(T), K => Type.Tuple([Type.Index(T, K), Type.Index(T, K)])) + Expect(D).ToStatic<{ x: [number, number] }> + + const E = Type.Mapped(Type.KeyOf(T), K => Type.Union([Type.Index(T, K)])) + Expect(E).ToStatic<{ x: number }> + + const F = Type.Mapped(Type.KeyOf(T), K => Type.Intersect([Type.Index(T, K)])) + Expect(F).ToStatic<{ x: number }> +} +// prettier-ignore +{ // Modifiers + const T = Type.Object({ + x: Type.Optional(Type.Number()), + y: Type.Number() + }) + // Additive + const A = Type.Mapped(Type.KeyOf(T), K => Type.Optional(Type.Index(T, K), true)) + Expect(A).ToStatic<{ x?: number, y?: number}>() + // Subtractive + const S = Type.Mapped(Type.KeyOf(T), K => Type.Optional(Type.Index(T, K), false)) + Expect(S).ToStatic<{ x: number, y: number}>() +} +// prettier-ignore +{ // Modifiers + const T = Type.Object({ + x: Type.Readonly(Type.Number()), + y: Type.Number() + }) + // Additive + const A = Type.Mapped(Type.KeyOf(T), K => Type.Readonly(Type.Index(T, K), true)) + Expect(A).ToStatic<{ readonly x: number, readonly y: number}>() + // Subtractive + const S = Type.Mapped(Type.KeyOf(T), K => Type.Readonly(Type.Index(T, K), false)) + Expect(S).ToStatic<{ x: number, y: number}>() +} +// ------------------------------------------------------------------ +// Finite Boolean +// ------------------------------------------------------------------ +{ + const T = Type.TemplateLiteral('${boolean}') + const M = Type.Mapped(T, (K) => K) + Expect(M).ToStatic<{ + true: 'true' + false: 'false' + }> +} +{ + const T = Type.TemplateLiteral('${0|1}${boolean}') + const M = Type.Mapped(T, (K) => K) + Expect(M).ToStatic<{ + '0true': '0true' + '0false': '0false' + '1true': '1true' + '1false': '1false' + }> +} +{ + const T = Type.TemplateLiteral('${boolean}${0|1}') + const M = Type.Mapped(T, (K) => K) + Expect(M).ToStatic<{ + true0: 'true0' + false0: 'false0' + true1: 'true1' + false1: 'false1' + }> +} +{ + const T = Type.TemplateLiteral([Type.Union([Type.Literal(0), Type.Literal(1)]), Type.Union([Type.Literal(0), Type.Literal(1)])]) + const M = Type.Mapped(T, (K) => K) + Expect(M).ToStatic<{ + '00': '00' + '01': '01' + '10': '10' + '11': '11' + }> +} +{ + const T = Type.Object({ + hello: Type.Number(), + world: Type.String(), + }) + const M = Type.Mapped(Type.Uppercase(Type.KeyOf(T)), (K) => { + return Type.Index(T, Type.Lowercase(K)) + }) + Expect(M).ToStatic<{ + HELLO: number + WORLD: string + }> +} +// ------------------------------------------------------------------ +// Interior Partial +// ------------------------------------------------------------------ +{ + const T = Type.Object({ + x: Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + y: Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + }) + const M = Type.Mapped(Type.KeyOf(T), (K) => { + return Type.Partial(Type.Index(T, K)) + }) + Expect(M).ToStatic<{ + x: { x?: number; y?: number } + y: { x?: number; y?: number } + }> +} +// ------------------------------------------------------------------ +// Interior Required +// ------------------------------------------------------------------ +{ + const T = Type.Object({ + x: Type.Partial( + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + ), + y: Type.Partial( + Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + ), + }) + const M = Type.Mapped(Type.KeyOf(T), (K) => { + return Type.Required(Type.Index(T, K)) + }) + Expect(M).ToStatic<{ + x: { x: number; y: number } + y: { x: number; y: number } + }> +} +// ------------------------------------------------------------------ +// Pick With Key +// ------------------------------------------------------------------ +// prettier-ignore +{ + const T = Type.Object({ + x: Type.Object({ + x: Type.Number(), + y: Type.Number() + }), + y: Type.Object({ + x: Type.Number(), + y: Type.Number() + }) + }) + const M = Type.Mapped(Type.KeyOf(T), K => { + return Type.Pick(T, K) + }) + Expect(M).ToStatic<{ + x: { x: { x: number; y: number; }; }; + y: { y: { x: number; y: number; }; }; + }> +} +// ------------------------------------------------------------------ +// Pick With Result +// ------------------------------------------------------------------ +{ + const T = Type.Object({ + x: Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + y: Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + }) + const M = Type.Mapped(Type.KeyOf(T), (K) => { + return Type.Pick(Type.Index(T, K), ['x']) + }) + Expect(M).ToStatic<{ + x: { x: number } + y: { x: number } + }> +} +// ------------------------------------------------------------------ +// Omit With Key +// ------------------------------------------------------------------ +// prettier-ignore +{ + const T = Type.Object({ + x: Type.Object({ + x: Type.Number(), + y: Type.Number() + }), + y: Type.Object({ + x: Type.Number(), + y: Type.Number() + }) + }) + const M = Type.Mapped(Type.KeyOf(T), K => { + return Type.Omit(T, K) + }) + Expect(M).ToStatic<{ + x: { y: { x: number; y: number; }; }; + y: { x: { x: number; y: number; }; }; + }> +} +// ------------------------------------------------------------------ +// Omit With Result +// ------------------------------------------------------------------ +{ + const T = Type.Object({ + x: Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + y: Type.Object({ + x: Type.Number(), + y: Type.Number(), + }), + }) + const M = Type.Mapped(Type.KeyOf(T), (K) => { + return Type.Omit(Type.Index(T, K), ['x']) + }) + Expect(M).ToStatic<{ + x: { y: number } + y: { y: number } + }> +} +// ------------------------------------------------------------------ +// With Enum +// issue: https://github.com/sinclairzx81/typebox/issues/897 +// ------------------------------------------------------------------ +{ + enum E { + A, + B, + } + const T = Type.Object({ a: Type.Enum(E) }) + const M = Type.Mapped(Type.KeyOf(T), (K) => Type.Index(T, K)) + Expect(M).ToStatic<{ a: E }> +} diff --git a/test/static/modifier.ts b/test/static/modifier.ts new file mode 100644 index 000000000..cb108b6d3 --- /dev/null +++ b/test/static/modifier.ts @@ -0,0 +1,18 @@ +import { Expect } from './assert' +import { Type, TSchema } from '@sinclair/typebox' + +// Asserts combinatory modifiers +{ + const T = Type.Object({ + A: Type.ReadonlyOptional(Type.String()), + B: Type.Readonly(Type.String()), + C: Type.Optional(Type.String()), + D: Type.String(), + }) + Expect(T).ToStatic<{ + readonly A?: string + readonly B: string + C?: string + D: string + }>() +} diff --git a/test/static/never.ts b/test/static/never.ts new file mode 100644 index 000000000..18dc62564 --- /dev/null +++ b/test/static/never.ts @@ -0,0 +1,7 @@ +import { Expect } from './assert' +import { Type } from '@sinclair/typebox' + +{ + const T = Type.Never() + Expect(T).ToStaticNever() +} diff --git a/test/static/not.ts b/test/static/not.ts new file mode 100644 index 000000000..5c95fa0ce --- /dev/null +++ b/test/static/not.ts @@ -0,0 +1,26 @@ +import { Expect } from './assert' +import { Type } from '@sinclair/typebox' + +{ + // ------------------------------------------------------------------------- + // Issue: type T = number extends not number ? true : false // true + // type T = number extends unknown ? true : false // true + // + // TypeScript does not support type negation. The best TypeBox can do is + // treat "not" as "unknown". From this standpoint, the extends assignability + // check needs to return true for the following case to keep TypeBox aligned + // with TypeScript static inference. + // ------------------------------------------------------------------------- + const A = Type.Number() + const B = Type.Not(Type.Number()) + const T = Type.Extends(A, B, Type.Literal(true), Type.Literal(false)) + Expect(T).ToStatic() +} +{ + const T = Type.Not(Type.Number()) + Expect(T).ToStatic() +} +{ + const T = Type.Not(Type.Not(Type.Number())) + Expect(T).ToStatic() +} diff --git a/test/static/null.ts b/test/static/null.ts new file mode 100644 index 000000000..50f474bec --- /dev/null +++ b/test/static/null.ts @@ -0,0 +1,4 @@ +import { Expect } from './assert' +import { Type } from '@sinclair/typebox' + +Expect(Type.Null()).ToStatic() diff --git a/test/static/number.ts b/test/static/number.ts new file mode 100644 index 000000000..17a41ee5c --- /dev/null +++ b/test/static/number.ts @@ -0,0 +1,4 @@ +import { Expect } from './assert' +import { Type } from '@sinclair/typebox' + +Expect(Type.Number()).ToStatic() diff --git a/test/static/object.ts b/test/static/object.ts new file mode 100644 index 000000000..d7f4b0ef5 --- /dev/null +++ b/test/static/object.ts @@ -0,0 +1,70 @@ +import { Expect } from './assert' +import { Type } from '@sinclair/typebox' + +{ + const T = Type.Object({ + A: Type.String(), + B: Type.String(), + C: Type.String(), + }) + + Expect(T).ToStatic<{ + A: string + B: string + C: string + }>() +} +{ + const T = Type.Object({ + A: Type.Object({ + A: Type.String(), + B: Type.String(), + C: Type.String(), + }), + B: Type.Object({ + A: Type.String(), + B: Type.String(), + C: Type.String(), + }), + C: Type.Object({ + A: Type.String(), + B: Type.String(), + C: Type.String(), + }), + }) + Expect(T).ToStatic<{ + A: { + A: string + B: string + C: string + } + B: { + A: string + B: string + C: string + } + C: { + A: string + B: string + C: string + } + }>() +} +{ + const T = Type.Object( + { + A: Type.Number(), + B: Type.Number(), + C: Type.Number(), + }, + { + additionalProperties: Type.Boolean(), + }, + ) + // note: Pending TypeScript support for negated types. + Expect(T).ToStatic<{ + A: number + B: number + C: number + }>() +} diff --git a/test/static/omit.ts b/test/static/omit.ts new file mode 100644 index 000000000..7b1bb5644 --- /dev/null +++ b/test/static/omit.ts @@ -0,0 +1,104 @@ +import { Expect } from './assert' +import { Type, Static } from '@sinclair/typebox' + +{ + const A = Type.Object({ + A: Type.String(), + B: Type.String(), + C: Type.String(), + }) + + const T = Type.Omit(A, ['A', 'B']) + + type T = Static + + Expect(T).ToStatic<{ + C: string + }>() +} + +{ + const A = Type.Object({ + A: Type.String(), + B: Type.String(), + C: Type.String(), + }) + + const keys = ['A', 'B'] as const + + const T = Type.Omit(A, keys) + + type T = Static + + Expect(T).ToStatic<{ + C: string + }>() +} + +{ + const A = Type.Object({ + A: Type.String(), + B: Type.String(), + C: Type.String(), + }) + const B = Type.Object({ + A: Type.String(), + B: Type.String(), + }) + + const T = Type.Omit(A, Type.KeyOf(B)) + + type T = Static + + Expect(T).ToStatic<{ + C: string + }>() +} +{ + const A = Type.Object({ type: Type.Literal('A') }) + const B = Type.Object({ type: Type.Literal('B') }) + const C = Type.Object({ type: Type.Literal('C') }) + const Union = Type.Union([A, B, C]) + const Extended = Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }) + const T = Type.Intersect([Union, Extended]) + + Expect(T).ToStatic< + ( + | { + type: 'A' + } + | { + type: 'B' + } + | { + type: 'C' + } + ) & { + x: number + y: number + z: number + } + >() + + const P = Type.Omit(T, ['type', 'x']) + + Expect(P).ToStatic< + ({} | {} | {}) & { + y: number + z: number + } + >() + + const O = Type.Partial(P) + + Expect(O).ToStatic< + ({} | {} | {}) & { + y?: number | undefined + z?: number | undefined + } + >() +} diff --git a/test/static/optional.ts b/test/static/optional.ts new file mode 100644 index 000000000..619930dc5 --- /dev/null +++ b/test/static/optional.ts @@ -0,0 +1,49 @@ +import { Expect } from './assert' +import { Type, Static } from '@sinclair/typebox' + +{ + const T = Type.Object({ + A: Type.Optional(Type.String()), + }) + type T = Static + + Expect(T).ToStatic<{ + A?: string + }>() +} +// Noop +// prettier-ignore +{ + const T = Type.Object({ + A: Type.Optional(Type.String(), false), + }) + type T = Static + + Expect(T).ToStatic<{ + A: string + }>() +} +// Additive +// prettier-ignore +{ + const T = Type.Object({ + A: Type.Optional(Type.String(), true), + }) + type T = Static + + Expect(T).ToStatic<{ + A?: string + }>() +} +// Subtractive +// prettier-ignore +{ + const T = Type.Object({ + A: Type.Optional(Type.Optional(Type.String()), false) + }) + type T = Static + + Expect(T).ToStatic<{ + A: string + }>() +} diff --git a/test/static/parameters.ts b/test/static/parameters.ts new file mode 100644 index 000000000..439ee9efe --- /dev/null +++ b/test/static/parameters.ts @@ -0,0 +1,13 @@ +import { Expect } from './assert' +import { Type } from '@sinclair/typebox' + +const C = Type.Function( + [Type.Number(), Type.String()], + Type.Object({ + method: Type.Function([Type.Number(), Type.String()], Type.Boolean()), + }), +) + +const P = Type.Parameters(C) + +Expect(P).ToStatic<[number, string]>() diff --git a/test/static/partial.ts b/test/static/partial.ts new file mode 100644 index 000000000..5feacceef --- /dev/null +++ b/test/static/partial.ts @@ -0,0 +1,105 @@ +import { Expect } from './assert' +import { Type, Static } from '@sinclair/typebox' +import * as Types from '@sinclair/typebox' +{ + const T = Type.Partial( + Type.Object({ + A: Type.String(), + B: Type.String(), + C: Type.String(), + }), + ) + type T = Static + + Expect(T).ToStatic<{ + A?: string + B?: string + C?: string + }>() +} +{ + { + const A = Type.Object({ type: Type.Literal('A') }) + const B = Type.Object({ type: Type.Literal('B') }) + const C = Type.Object({ type: Type.Literal('C') }) + const Union = Type.Union([A, B, C]) + const Extended = Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }) + + const T = Type.Intersect([Union, Extended]) + + Expect(T).ToStatic< + ( + | { + type: 'A' + } + | { + type: 'B' + } + | { + type: 'C' + } + ) & { + x: number + y: number + z: number + } + >() + + const P = Type.Partial(T) + + Expect(P).ToStatic< + ( + | { + type?: 'A' | undefined + } + | { + type?: 'B' | undefined + } + | { + type?: 'C' | undefined + } + ) & { + x?: number | undefined + y?: number | undefined + z?: number | undefined + } + >() + } +} +{ + // https://github.com/sinclairzx81/typebox/issues/655 + const T = Type.Object({ + a: Type.ReadonlyOptional(Type.Number()), + b: Type.Readonly(Type.Number()), + c: Type.Optional(Type.Number()), + d: Type.Number(), + }) + const R: Types.TObject<{ + a: Types.TReadonlyOptional + b: Types.TReadonlyOptional + c: Types.TOptional + d: Types.TOptional + }> = Type.Partial(T) +} +// ------------------------------------------------------------------ +// Intrinsic Passthough +// https://github.com/sinclairzx81/typebox/issues/1169 +// ------------------------------------------------------------------ +// prettier-ignore +{ + const T = Type.Partial(Type.Union([Type.Number(), Type.Object({ + x: Type.Number() + })])) + Expect(T).ToStatic +} +// prettier-ignore +{ + const T = Type.Partial(Type.Union([Type.Literal(1), Type.Object({ + x: Type.Number() + })])) + Expect(T).ToStatic<1 | { x?: number }> +} diff --git a/test/static/pick.ts b/test/static/pick.ts new file mode 100644 index 000000000..895cdefac --- /dev/null +++ b/test/static/pick.ts @@ -0,0 +1,129 @@ +import { Expect } from './assert' +import { Type, Static } from '@sinclair/typebox' + +{ + const A = Type.Object({ + A: Type.String(), + B: Type.String(), + C: Type.String(), + }) + + const T = Type.Pick(A, ['A', 'B']) + + type T = Static + + Expect(T).ToStatic<{ + A: string + B: string + }>() +} + +{ + const A = Type.Object({ + A: Type.String(), + B: Type.String(), + C: Type.String(), + }) + + const keys = ['A', 'B'] as const + + const T = Type.Pick(A, ['A', 'B']) + + type T = Static + + Expect(T).ToStatic<{ + A: string + B: string + }>() +} + +{ + const A = Type.Object({ + A: Type.String(), + B: Type.String(), + C: Type.String(), + }) + const B = Type.Object({ + A: Type.String(), + B: Type.String(), + }) + + const T = Type.Pick(A, Type.KeyOf(B)) + + type T = Static + + Expect(T).ToStatic<{ + A: string + B: string + }>() +} +{ + const A = Type.Object({ type: Type.Literal('A') }) + const B = Type.Object({ type: Type.Literal('B') }) + const C = Type.Object({ type: Type.Literal('C') }) + const Union = Type.Union([A, B, C]) + const Extended = Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }) + const T = Type.Intersect([Union, Extended]) + + Expect(T).ToStatic< + ( + | { + type: 'A' + } + | { + type: 'B' + } + | { + type: 'C' + } + ) & { + x: number + y: number + z: number + } + >() + + const K = Type.KeyOf(T) + + Expect(K).ToStatic<'type' | 'x' | 'y' | 'z'>() + + const P = Type.Pick(T, ['type', 'x']) + + Expect(P).ToStatic< + ( + | { + type: 'A' + } + | { + type: 'B' + } + | { + type: 'C' + } + ) & { + x: number + } + >() + + const O = Type.Partial(P) + + Expect(O).ToStatic< + ( + | { + type?: 'A' | undefined + } + | { + type?: 'B' | undefined + } + | { + type?: 'C' | undefined + } + ) & { + x?: number | undefined + } + >() +} diff --git a/test/static/readonly-optional.ts b/test/static/readonly-optional.ts new file mode 100644 index 000000000..f51696bd7 --- /dev/null +++ b/test/static/readonly-optional.ts @@ -0,0 +1,28 @@ +import { Expect } from './assert' +import { Type, TSchema, TReadonlyOptional } from '@sinclair/typebox' + +{ + const T = Type.Object({ + A: Type.ReadonlyOptional(Type.String()), + }) + Expect(T).ToStatic<{ + readonly A?: string + }>() +} +{ + const T = Type.ReadonlyOptional(Type.String()) + function test(_: TReadonlyOptional) {} + test(T) +} +{ + const T = Type.Readonly(Type.String()) + function test(_: TReadonlyOptional) {} + // @ts-expect-error + test(T) +} +{ + const T = Type.Optional(Type.String()) + function test(_: TReadonlyOptional) {} + // @ts-expect-error + test(T) +} diff --git a/test/static/readonly.ts b/test/static/readonly.ts new file mode 100644 index 000000000..1d6224fc5 --- /dev/null +++ b/test/static/readonly.ts @@ -0,0 +1,50 @@ +import { Expect } from './assert' +import { Type, Static } from '@sinclair/typebox' + +{ + const T = Type.Object({ + A: Type.Readonly(Type.String()), + }) + + type T = Static + + Expect(T).ToStatic<{ + readonly A: string + }>() +} +// Noop +// prettier-ignore +{ + const T = Type.Object({ + A: Type.Readonly(Type.String(), false), + }) + type T = Static + + Expect(T).ToStatic<{ + A: string + }>() +} +// Additive +// prettier-ignore +{ + const T = Type.Object({ + A: Type.Readonly(Type.String(), true), + }) + type T = Static + + Expect(T).ToStatic<{ + readonly A: string + }>() +} +// Subtractive +// prettier-ignore +{ + const T = Type.Object({ + A: Type.Readonly(Type.Readonly(Type.String()), false) + }) + type T = Static + + Expect(T).ToStatic<{ + A: string + }>() +} diff --git a/test/static/record.ts b/test/static/record.ts new file mode 100644 index 000000000..df66a813e --- /dev/null +++ b/test/static/record.ts @@ -0,0 +1,257 @@ +import { Expect } from './assert' +import { Type, Static } from '@sinclair/typebox' +{ + // type K = string + const K = Type.String() + const T = Type.Record(K, Type.Number()) + type T = Static + + Expect(T).ToStatic>() +} +{ + // type K = string + const K = Type.RegExp(/foo|bar/) + const T = Type.Record(K, Type.Number()) + type T = Static + Expect(T).ToStatic>() +} +{ + // type K = number + const K = Type.Number() + const T = Type.Record(K, Type.Number()) + type T = Static + Expect(T).ToStatic>() +} +{ + // type K = 'A' | 'B' | 'C' + const K = Type.Union([Type.Literal('A'), Type.Literal('B'), Type.Literal('C')]) + const T = Type.Record(K, Type.Number()) + type T = Static + Expect(T).ToStatic>() +} +{ + // type K = keyof { A: number, B: number, C: number } + const K = Type.KeyOf( + Type.Object({ + A: Type.Number(), + B: Type.Number(), + C: Type.Number(), + }), + ) + const T = Type.Record(K, Type.Number()) + type T = Static + Expect(T).ToStatic>() +} +{ + // type K = keyof Omit<{ A: number, B: number, C: number }, 'C'> + const K = Type.KeyOf( + Type.Omit( + Type.Object({ + A: Type.Number(), + B: Type.Number(), + C: Type.Number(), + }), + ['C'], + ), + ) + const T = Type.Record(K, Type.Number()) + type T = Static + Expect(T).ToStatic>() +} + +{ + const T = Type.Record(Type.Number(), Type.String()) + + Expect(T).ToStatic>() +} +{ + const T = Type.Record(Type.Integer(), Type.String()) + + Expect(T).ToStatic>() +} +{ + // Should support enum keys 1 + enum E { + A = 'X', + B = 'Y', + C = 'Z', + } + const T = Type.Record(Type.Enum(E), Type.Number()) + Expect(T).ToStatic<{ + X: number + Y: number + Z: number + }>() +} +{ + // Should support enum keys 2 + enum E { + A, + B, + C, + } + const T = Type.Record(Type.Enum(E), Type.Number()) + Expect(T).ToStatic<{ + 0: number + 1: number + 2: number + }>() +} +{ + // Should support enum keys 3 + enum E { + A = 1, + B = '2', + C = 'Z', + } + const T = Type.Record(Type.Enum(E), Type.Number()) + Expect(T).ToStatic<{ + 1: number + 2: number + Z: number + }>() +} +{ + // should support infinite record keys + // https://github.com/sinclairzx81/typebox/issues/604 + const K = Type.TemplateLiteral('key${number}') + const R = Type.Record(K, Type.Number()) + Expect(R).ToStatic>() +} +{ + // should support infinite record keys with intersect + // https://github.com/sinclairzx81/typebox/issues/604 + const K = Type.TemplateLiteral('key${number}') + const R = Type.Record(K, Type.Number()) + const T = Type.Object({ x: Type.Number(), y: Type.Number() }) + const I = Type.Intersect([R, T]) + Expect(I).ToStatic & { x: number; y: number }>() +} +{ + // expect T as Object + enum E { + A, + B, + C, + } + const T = Type.Record(Type.Enum(E), Type.Number()) + Expect(T).ToStatic<{ + 0: number + 1: number + 2: number + }> +} +{ + // expect T as Partial Object + enum E { + A, + B, + C, + } + const T = Type.Partial(Type.Record(Type.Enum(E), Type.Number())) + Expect(T).ToStatic<{ + 0?: number + 1?: number + 2?: number + }> +} +{ + // expect T to support named properties + enum E { + A = 'A', + B = 'B', + C = 'C', + } + const T = Type.Record(Type.Enum(E), Type.Number()) + Expect(T).ToStatic<{ + A: number + B: number + C: number + }> +} +{ + // expect T to support named properties + enum E {} + const T = Type.Record(Type.Enum(E), Type.Number()) + Expect(T).ToStatic<{ [x: string]: number }> +} +// ------------------------------------------------------------------ +// Dollar Sign Escape +// https://github.com/sinclairzx81/typebox/issues/794 +// ------------------------------------------------------------------ +// prettier-ignore +{ + const K = Type.TemplateLiteral('$prop${A|B|C}') // issue + const T = Type.Record(K, Type.String()) + Expect(T).ToStatic<{ + '$propA': string, + '$propB': string, + '$propC': string + }>() +} +// ------------------------------------------------------------------ +// https://github.com/sinclairzx81/typebox/issues/916 +// ------------------------------------------------------------------ +{ + const K = Type.Any() + const T = Type.Record(K, Type.String()) + Expect(T).ToStatic>() +} +{ + const K = Type.Never() + const T = Type.Record(K, Type.String()) + Expect(T).ToStatic<{}>() +} +// ------------------------------------------------------------------ +// Deep Union +// https://github.com/sinclairzx81/typebox/issues/1208 +// ------------------------------------------------------------------ +// prettier-ignore +{ + const A = Type.Record(Type.Union([ + Type.Literal(0), Type.Literal(1), Type.Literal(2), Type.Literal(3), Type.Literal(4), Type.Literal(5), Type.Literal(6), Type.Literal(7), + Type.Literal(8), Type.Literal(9), Type.Literal(10), Type.Literal(11), Type.Literal(12), Type.Literal(13), Type.Literal(14), Type.Literal(15), + Type.Literal(16), Type.Literal(17), Type.Literal(18), Type.Literal(19), Type.Literal(20), Type.Literal(21), Type.Literal(22), Type.Literal(23), + Type.Literal(24), Type.Literal(25), Type.Literal(26), Type.Literal(27), Type.Literal(28), Type.Literal(29), Type.Literal(30), Type.Literal(31), + Type.Literal(32), Type.Literal(33), Type.Literal(34), Type.Literal(35), Type.Literal(36), Type.Literal(37), Type.Literal(38), Type.Literal(39), + Type.Literal(40), Type.Literal(41), Type.Literal(42), Type.Literal(43), Type.Literal(44), Type.Literal(45), Type.Literal(46), Type.Literal(47), + Type.Literal(48), Type.Literal(49), Type.Literal(50), Type.Literal(51), Type.Literal(52), Type.Literal(53), Type.Literal(54), Type.Literal(55), + Type.Literal(56), Type.Literal(57), Type.Literal(58), Type.Literal(59), Type.Literal(60), Type.Literal(61), Type.Literal(62), Type.Literal(63), // <- x64 + Type.Literal(64), Type.Literal(65), Type.Literal(66), Type.Literal(67), Type.Literal(68), Type.Literal(69), Type.Literal(70), Type.Literal(71), + Type.Literal(72), Type.Literal(73), Type.Literal(74), Type.Literal(75), Type.Literal(76), Type.Literal(77), Type.Literal(78), Type.Literal(79), + Type.Literal(80), Type.Literal(81), Type.Literal(82), Type.Literal(83), Type.Literal(84), Type.Literal(85), Type.Literal(86), Type.Literal(87), + Type.Literal(88), Type.Literal(89), Type.Literal(90), Type.Literal(91), Type.Literal(92), Type.Literal(93), Type.Literal(94), Type.Literal(95), + Type.Literal(96), Type.Literal(97), Type.Literal(98), Type.Literal(99), Type.Literal(100), Type.Literal(101), Type.Literal(102), Type.Literal(103), + Type.Literal(104), Type.Literal(105), Type.Literal(106), Type.Literal(107), Type.Literal(108), Type.Literal(109), Type.Literal(110), Type.Literal(111), + Type.Literal(112), Type.Literal(113), Type.Literal(114), Type.Literal(115), Type.Literal(116), Type.Literal(117), Type.Literal(118), Type.Literal(119), + Type.Literal(120), Type.Literal(121), Type.Literal(122), Type.Literal(123), Type.Literal(124), Type.Literal(125), Type.Literal(126), Type.Literal(127), // <- x128 + ]), Type.String()) + const B = Type.Record(Type.Union([ + Type.Union([ + Type.Literal(0), Type.Literal(1), Type.Literal(2), Type.Literal(3), Type.Literal(4), Type.Literal(5), Type.Literal(6), Type.Literal(7), + Type.Literal(8), Type.Literal(9), Type.Literal(10), Type.Literal(11), Type.Literal(12), Type.Literal(13), Type.Literal(14), Type.Literal(15), + Type.Literal(16), Type.Literal(17), Type.Literal(18), Type.Literal(19), Type.Literal(20), Type.Literal(21), Type.Literal(22), Type.Literal(23), + Type.Literal(24), Type.Literal(25), Type.Literal(26), Type.Literal(27), Type.Literal(28), Type.Literal(29), Type.Literal(30), Type.Literal(31), + ]), + Type.Union([ + Type.Literal(32), Type.Literal(33), Type.Literal(34), Type.Literal(35), Type.Literal(36), Type.Literal(37), Type.Literal(38), Type.Literal(39), + Type.Literal(40), Type.Literal(41), Type.Literal(42), Type.Literal(43), Type.Literal(44), Type.Literal(45), Type.Literal(46), Type.Literal(47), + Type.Literal(48), Type.Literal(49), Type.Literal(50), Type.Literal(51), Type.Literal(52), Type.Literal(53), Type.Literal(54), Type.Literal(55), + Type.Literal(56), Type.Literal(57), Type.Literal(58), Type.Literal(59), Type.Literal(60), Type.Literal(61), Type.Literal(62), Type.Literal(63), // <- x64 + ]), + Type.Union([ + Type.Literal(64), Type.Literal(65), Type.Literal(66), Type.Literal(67), Type.Literal(68), Type.Literal(69), Type.Literal(70), Type.Literal(71), + Type.Literal(72), Type.Literal(73), Type.Literal(74), Type.Literal(75), Type.Literal(76), Type.Literal(77), Type.Literal(78), Type.Literal(79), + Type.Literal(80), Type.Literal(81), Type.Literal(82), Type.Literal(83), Type.Literal(84), Type.Literal(85), Type.Literal(86), Type.Literal(87), + Type.Literal(88), Type.Literal(89), Type.Literal(90), Type.Literal(91), Type.Literal(92), Type.Literal(93), Type.Literal(94), Type.Literal(95), + ]), + Type.Union([ + Type.Literal(96), Type.Literal(97), Type.Literal(98), Type.Literal(99), Type.Literal(100), Type.Literal(101), Type.Literal(102), Type.Literal(103), + Type.Literal(104), Type.Literal(105), Type.Literal(106), Type.Literal(107), Type.Literal(108), Type.Literal(109), Type.Literal(110), Type.Literal(111), + Type.Literal(112), Type.Literal(113), Type.Literal(114), Type.Literal(115), Type.Literal(116), Type.Literal(117), Type.Literal(118), Type.Literal(119), + Type.Literal(120), Type.Literal(121), Type.Literal(122), Type.Literal(123), Type.Literal(124), Type.Literal(125), Type.Literal(126), Type.Literal(127), // <- x128 + ]) + ]), Type.String()) + type A = Static + Expect(B).ToStatic() +} diff --git a/test/static/recursive.ts b/test/static/recursive.ts new file mode 100644 index 000000000..3c4cc9aee --- /dev/null +++ b/test/static/recursive.ts @@ -0,0 +1,100 @@ +import { Static, Type } from '@sinclair/typebox' +import { Expect } from './assert' + +{ + // identity + const R = Type.Recursive((Node) => + Type.Object({ + id: Type.String(), + nodes: Type.Array(Node), + }), + ) + type T = Static + Expect(R).ToStatic<{ id: string; nodes: T[] }>() +} +{ + // keyof + const R = Type.Recursive((Node) => + Type.Object({ + id: Type.String(), + nodes: Type.Array(Node), + }), + ) + const T = Type.KeyOf(R) + Expect(T).ToStatic<'id' | 'nodes'>() +} +{ + // partial + const R = Type.Recursive((Node) => + Type.Object({ + id: Type.String(), + nodes: Type.Array(Node), + }), + ) + const T = Type.Partial(R) + Expect(T).ToStatic<{ + id?: string | undefined + nodes?: Static[] | undefined + }>() +} +{ + // required + const R = Type.Recursive((Node) => + Type.Object({ + id: Type.String(), + nodes: Type.Array(Node), + }), + ) + const P = Type.Partial(R) + const T = Type.Required(P) + Expect(T).ToStatic<{ + id: string + nodes: Static[] + }>() +} +{ + // pick + const R = Type.Recursive((Node) => + Type.Object({ + id: Type.String(), + nodes: Type.Array(Node), + }), + ) + const T = Type.Pick(R, ['id']) + Expect(T).ToStatic<{ + id: string + }>() +} +{ + // omit + const R = Type.Recursive((Node) => + Type.Object({ + id: Type.String(), + nodes: Type.Array(Node), + }), + ) + const T = Type.Omit(R, ['id']) + Expect(T).ToStatic<{ + nodes: Static[] + }>() +} +// prettier-ignore +{ + // issue: https://github.com/sinclairzx81/typebox/issues/336 + type JSONValue = + | string + | number + | null + | boolean + | { [x: string]: JSONValue } + | JSONValue[] + const R = Type.Recursive((Node) => Type.Union([ + Type.Null(), + Type.String(), + Type.Number(), + Type.Boolean(), + Type.Record(Type.String(), Node), + Type.Array(Node) + ])) + Expect(R).ToStatic() +} diff --git a/test/static/ref.ts b/test/static/ref.ts new file mode 100644 index 000000000..76aca672b --- /dev/null +++ b/test/static/ref.ts @@ -0,0 +1,13 @@ +import { Expect } from './assert' +import { Type, Static } from '@sinclair/typebox' + +{ + const T = Type.String({ $id: 'T' }) + const R = Type.Ref('T') + + type T = Static + type R = Static + + Expect(T).ToStatic() + Expect(R).ToStatic() +} diff --git a/test/static/regexp.ts b/test/static/regexp.ts new file mode 100644 index 000000000..7f9413071 --- /dev/null +++ b/test/static/regexp.ts @@ -0,0 +1,4 @@ +import { Expect } from './assert' +import { Type } from '@sinclair/typebox' + +Expect(Type.RegExp(/foo/)).ToStatic() diff --git a/test/static/required.ts b/test/static/required.ts new file mode 100644 index 000000000..47c7a3be7 --- /dev/null +++ b/test/static/required.ts @@ -0,0 +1,107 @@ +import { Expect } from './assert' +import { Type, Static } from '@sinclair/typebox' +import * as Types from '@sinclair/typebox' +{ + const T = Type.Required( + Type.Object({ + A: Type.Optional(Type.String()), + B: Type.Optional(Type.String()), + C: Type.Optional(Type.String()), + }), + ) + + type T = Static + + Expect(T).ToStatic<{ + A: string + B: string + C: string + }>() +} +{ + { + const A = Type.Object({ type: Type.Literal('A') }) + const B = Type.Object({ type: Type.Literal('B') }) + const C = Type.Object({ type: Type.Literal('C') }) + const Union = Type.Union([A, B, C]) + const Extended = Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }) + const T = Type.Intersect([Union, Extended]) + + const P = Type.Partial(T) + + Expect(P).ToStatic< + ( + | { + type?: 'A' | undefined + } + | { + type?: 'B' | undefined + } + | { + type?: 'C' | undefined + } + ) & { + x?: number | undefined + y?: number | undefined + z?: number | undefined + } + >() + + const R = Type.Required(P) + + Expect(R).ToStatic< + ( + | { + type: 'A' + } + | { + type: 'B' + } + | { + type: 'C' + } + ) & { + x: number + y: number + z: number + } + >() + } +} +{ + // https://github.com/sinclairzx81/typebox/issues/655 + const T = Type.Object({ + a: Type.ReadonlyOptional(Type.Number()), + b: Type.Readonly(Type.Number()), + c: Type.Optional(Type.Number()), + d: Type.Number(), + }) + const R: Types.TObject<{ + a: Types.TReadonly + b: Types.TReadonly + c: Types.TNumber + d: Types.TNumber + }> = Type.Required(T) +} +// ------------------------------------------------------------------ +// Intrinsic Passthough +// https://github.com/sinclairzx81/typebox/issues/1169 +// ------------------------------------------------------------------ +// prettier-ignore +{ + const T = Type.Required(Type.Union([Type.Number(), Type.Object({ + x: Type.Optional(Type.Number()) + })])) + Expect(T).ToStatic +} +// prettier-ignore +{ + const T = Type.Required(Type.Union([Type.Literal(1), Type.Object({ + x: Type.Optional(Type.Number()) + })])) + Expect(T).ToStatic<1 | { x: number }> +} diff --git a/test/static/rest.ts b/test/static/rest.ts new file mode 100644 index 000000000..0d364a406 --- /dev/null +++ b/test/static/rest.ts @@ -0,0 +1,52 @@ +import { Expect } from './assert' +import { Type, Static } from '@sinclair/typebox' +{ + // union never + const A = Type.String() + const B = Type.Union(Type.Rest(A)) + Expect(B).ToStaticNever() +} +{ + // intersect never + const A = Type.String() + const B = Type.Intersect(Type.Rest(A)) + Expect(B).ToStaticNever() +} +{ + // tuple + const A = Type.Tuple([Type.Number(), Type.String()]) + const B = Type.Union(Type.Rest(A)) + Expect(B).ToStatic() +} +{ + // tuple spread + const A = Type.Tuple([Type.Literal(1), Type.Literal(2)]) + const B = Type.Tuple([Type.Literal(3), Type.Literal(4)]) + const C = Type.Tuple([...Type.Rest(A), ...Type.Rest(B)]) + Expect(C).ToStatic<[1, 2, 3, 4]>() +} +{ + // union to intersect + const A = Type.Object({ x: Type.Number() }) + const B = Type.Object({ y: Type.String() }) + const C = Type.Union([A, B]) + const D = Type.Intersect(Type.Rest(C)) + Expect(D).ToStatic< + { + x: number + } & { + y: string + } + >() +} +{ + // intersect to composite + const A = Type.Object({ x: Type.Number() }) + const B = Type.Object({ y: Type.String() }) + const C = Type.Intersect([A, B]) + const D = Type.Composite(Type.Rest(C)) + Expect(D).ToStatic<{ + x: number + y: string + }>() +} diff --git a/test/static/return-type.ts b/test/static/return-type.ts new file mode 100644 index 000000000..aa463509b --- /dev/null +++ b/test/static/return-type.ts @@ -0,0 +1,18 @@ +import { Expect } from './assert' +import { Type, Static } from '@sinclair/typebox' + +{ + const T = Type.ReturnType(Type.Function([], Type.String())) + + type T = Static + + Expect(T).ToStatic() +} + +{ + const T = Type.ReturnType(Type.Function([Type.Number()], Type.Number())) + + type T = Static + + Expect(T).ToStatic() +} diff --git a/test/static/string.ts b/test/static/string.ts new file mode 100644 index 000000000..609dad5ad --- /dev/null +++ b/test/static/string.ts @@ -0,0 +1,4 @@ +import { Expect } from './assert' +import { Type } from '@sinclair/typebox' + +Expect(Type.String()).ToStatic() diff --git a/test/static/symbol.ts b/test/static/symbol.ts new file mode 100644 index 000000000..13c8c6aeb --- /dev/null +++ b/test/static/symbol.ts @@ -0,0 +1,4 @@ +import { Expect } from './assert' +import { Type } from '@sinclair/typebox' + +Expect(Type.Symbol()).ToStatic() diff --git a/test/static/syntax.ts b/test/static/syntax.ts new file mode 100644 index 000000000..7b0a84601 --- /dev/null +++ b/test/static/syntax.ts @@ -0,0 +1,35 @@ +import { Expect } from './assert' +import { Syntax } from '@sinclair/typebox/syntax' + +// prettier-ignore +{ + const Basis = Syntax(`{ + x: 1, + y: 2, + z: 3 + }`) + + Expect(Basis).ToStatic<{ + x: 1, + y: 2, + z: 3, + }>() +} +// prettier-ignore +{ + const Vector = Syntax(`{ + x: X, + y: Y, + z: Z + }`) + const Basis = Syntax({ Vector }, `{ + x: Vector<1, 0, 0>, + y: Vector<0, 1, 0>, + z: Vector<0, 0, 1>, + }`) + Expect(Basis).ToStatic<{ + x: { x: 1, y: 0, z: 0 }, + y: { x: 0, y: 1, z: 0 }, + z: { x: 0, y: 0, z: 1 }, + }>() +} diff --git a/test/static/template-literal.ts b/test/static/template-literal.ts new file mode 100644 index 000000000..574d0e949 --- /dev/null +++ b/test/static/template-literal.ts @@ -0,0 +1,129 @@ +import { Expect } from './assert' +import { Type } from '@sinclair/typebox' + +{ + // Empty + const T = Type.TemplateLiteral([]) + Expect(T).ToStatic<''>() +} +{ + // Literal + const T = Type.TemplateLiteral([Type.Literal('hello')]) + Expect(T).ToStatic<'hello'>() +} +{ + // And Sequence + const T = Type.TemplateLiteral([Type.Literal('hello'), Type.Literal('world')]) + Expect(T).ToStatic<'helloworld'>() +} +{ + // And / Or Sequence + const T = Type.TemplateLiteral([Type.Literal('hello'), Type.Union([Type.Literal('1'), Type.Literal('2')])]) + Expect(T).ToStatic<'hello1' | 'hello2'>() +} +{ + // Auxiliary Template + const A = Type.TemplateLiteral([Type.Union([Type.Literal('1'), Type.Literal('2')])]) + const T = Type.TemplateLiteral([Type.Literal('hello'), A]) + Expect(T).ToStatic<'hello1' | 'hello2'>() +} +{ + // TemplateLiteral Composition + const A = Type.TemplateLiteral('${A|B}') + const B = Type.TemplateLiteral('${C|D}') + const T = Type.TemplateLiteral([A, B]) + Expect(T).ToStatic<'AC' | 'AD' | 'BC' | 'BD'>() +} +{ + // String + const T = Type.TemplateLiteral([Type.String()]) + Expect(T).ToStatic<`${string}`>() +} +{ + // Number + const T = Type.TemplateLiteral([Type.Number()]) + Expect(T).ToStatic<`${number}`>() +} +{ + // Boolean + const T = Type.TemplateLiteral([Type.Boolean()]) + Expect(T).ToStatic<`${boolean}`>() +} +{ + // Enum Implicit + enum E { + A, + B, + C, + } + const A = Type.Enum(E) + const T = Type.TemplateLiteral([Type.Literal('hello'), A]) + Expect(T).ToStatic<'hello0' | 'hello1' | 'hello2'>() +} +{ + // Enum Explicit + enum E { + A, + B = 'B', + C = 'C', + } + const A = Type.Enum(E) + const T = Type.TemplateLiteral([Type.Literal('hello'), A]) + Expect(T).ToStatic<'hello0' | 'helloB' | 'helloC'>() +} +{ + // Enum Object Explicit + const A = Type.Enum(Object.freeze({ a: 'A', b: 'B' })) + const T = Type.TemplateLiteral([Type.Literal('hello'), A]) + Expect(T).ToStatic<'helloA' | 'helloB'>() +} +// ------------------------------------------------------------------ +// Dollar Sign Escape +// https://github.com/sinclairzx81/typebox/issues/794 +// ------------------------------------------------------------------ +// prettier-ignore +{ + const T = Type.TemplateLiteral('$prop${A|B|C}') // issue + Expect(T).ToStatic<'$propA' | '$propB' | '$propC'>() +} +// prettier-ignore +{ + const T = Type.TemplateLiteral('$prop${A|B|C}x') // trailing + Expect(T).ToStatic<'$propAx' | '$propBx' | '$propCx'>() +} +// prettier-ignore +{ + const T = Type.TemplateLiteral('$prop${A|B|C}x}') // non-greedy + Expect(T).ToStatic<'$propAx}' | '$propBx}' | '$propCx}'>() +} +// prettier-ignore +{ + const T = Type.TemplateLiteral('$prop${A|B|C}x}${X|Y}') // distributive - non-greedy + Expect(T).ToStatic< + '$propAx}X' | '$propBx}X' | '$propCx}X' | + '$propAx}Y' | '$propBx}Y' | '$propCx}Y' + >() +} +// prettier-ignore +{ + const T = Type.TemplateLiteral('$prop${A|B|C}x}${X|Y}x') // distributive - non-greedy - trailing + Expect(T).ToStatic< + '$propAx}Xx' | '$propBx}Xx' | '$propCx}Xx' | + '$propAx}Yx' | '$propBx}Yx' | '$propCx}Yx' + >() +} +// --------------------------------------------------------------------- +// issue: https://github.com/sinclairzx81/typebox/issues/913 +// --------------------------------------------------------------------- +{ + const A = Type.TemplateLiteral([Type.Union([Type.Literal('A'), Type.Literal('B')])]) + const B = Type.TemplateLiteral([Type.Union([Type.Literal('X'), Type.Literal('Y')])]) + const L = Type.TemplateLiteral([Type.Literal('KEY'), A, B]) + const T = Type.Mapped(L, (K) => Type.Null()) + Expect(T).ToStatic<{ + KEYAX: null + KEYAY: null + KEYBX: null + KEYBY: null + }>() +} diff --git a/test/static/transform.ts b/test/static/transform.ts new file mode 100644 index 000000000..e9dbc2177 --- /dev/null +++ b/test/static/transform.ts @@ -0,0 +1,376 @@ +import { Type, TSchema, Static, StaticDecode, TObject, TNumber } from '@sinclair/typebox' +import { TypeCheck } from '@sinclair/typebox/compiler' +import { Value } from '@sinclair/typebox/value' + +import { Expect } from './assert' +{ + // string > number + const T = Type.Transform(Type.Number()) + .Decode((value) => value.toString()) + .Encode((value) => parseFloat(value)) + + Expect(T).ToStaticDecode() + Expect(T).ToStaticEncode() + Expect(T).ToStatic() +} +{ + // boolean > union + const T = Type.Transform(Type.Boolean()) + .Decode((value) => (value ? (1 as const) : (2 as const))) + .Encode((value) => true) + + Expect(T).ToStatic() + Expect(T).ToStaticDecode<1 | 2>() + Expect(T).ToStaticEncode() +} +{ + // literal > union + const T = Type.Transform(Type.Union([Type.Literal(1), Type.Literal(2)])) + .Decode((value) => true as const) + .Encode((value) => (value ? (1 as const) : (2 as const))) + Expect(T).ToStatic<1 | 2>() + Expect(T).ToStaticDecode() + Expect(T).ToStaticEncode<1 | 2>() +} +{ + // nested: 1 > 2 > 3 + const T1 = Type.Transform(Type.Literal(1)) + .Decode((value) => 2 as const) + .Encode((value) => 1 as const) + + const T2 = Type.Transform(T1) + .Decode((value) => 3 as const) + .Encode((value) => 2 as const) + + Expect(T1).ToStatic<1>() + Expect(T1).ToStaticDecode<2>() + Expect(T1).ToStaticEncode<1>() + + Expect(T2).ToStatic<2>() // resolve to base + Expect(T2).ToStaticDecode<3>() + Expect(T2).ToStaticEncode<2>() +} +{ + // nested: 1 > 2 > 3 > 4 + const T1 = Type.Transform(Type.Literal(1)) + .Decode((value) => 2 as const) + .Encode((value) => 1 as const) + + const T2 = Type.Transform(T1) + .Decode((value) => 3 as const) + .Encode((value) => 2 as const) + + const T3 = Type.Transform(T2) + .Decode((value) => 4 as const) + .Encode((value) => 3 as const) + + Expect(T1).ToStatic<1>() + Expect(T1).ToStaticDecode<2>() + Expect(T1).ToStaticEncode<1>() + + Expect(T2).ToStatic<2>() + Expect(T2).ToStaticDecode<3>() + Expect(T2).ToStaticEncode<2>() + + Expect(T3).ToStatic<3>() + Expect(T3).ToStaticDecode<4>() + Expect(T3).ToStaticEncode<3>() +} +{ + // recursive > 1 + // prettier-ignore + const T = Type.Transform(Type.Recursive(This => Type.Object({ + id: Type.String(), + nodes: Type.Array(This) + }))) + .Decode((value) => 1 as const) + .Encode((value) => ({ + id: 'A', + nodes: [ + { id: 'B', nodes: [] }, + { id: 'C', nodes: [] } + ] + })) + + interface N { + id: string + nodes: this[] + } + Expect(T).ToStatic() + Expect(T).ToStaticDecode<1>() + Expect(T).ToStaticEncode() +} +{ + // recursive > 1 > 2 + interface N { + id: string + nodes: this[] + } + // prettier-ignore + const T1 = Type.Transform(Type.Recursive(This => Type.Object({ + id: Type.String(), + nodes: Type.Array(This) + }))) + .Decode((value) => 1 as const) + .Encode((value) => ({ + id: 'A', + nodes: [ + { id: 'B', nodes: [] }, + { id: 'C', nodes: [] } + ] + })) + + const T2 = Type.Transform(T1) + .Decode((value) => 2 as const) + .Encode((value) => 1 as const) + + Expect(T1).ToStatic() + Expect(T1).ToStaticDecode<1>() + Expect(T1).ToStaticEncode() + + Expect(T2).ToStatic<1>() + Expect(T2).ToStaticDecode<2>() + Expect(T2).ToStaticEncode<1>() +} +{ + // deep-nesting + // prettier-ignore + const T = Type.Transform(Type.Transform(Type.Transform(Type.Transform(Type.Transform(Type.Transform(Type.Transform(Type.Transform(Type.Transform(Type.Literal(1)) + .Decode((value) => value) + .Encode((value) => value)) + .Decode((value) => value) + .Encode((value) => value)) + .Decode((value) => value) + .Encode((value) => value)) + .Decode((value) => value) + .Encode((value) => value)) + .Decode((value) => value) + .Encode((value) => value)) + .Decode((value) => value) + .Encode((value) => value)) + .Decode((value) => value) + .Encode((value) => value)) + .Decode((value) => value) + .Encode((value) => value)) + .Decode((value) => value) + .Encode((value) => value) + + Expect(T).ToStatic<1>() + Expect(T).ToStaticDecode<1>() + Expect(T).ToStaticEncode<1>() +} +{ + // null to typebox type + // prettier-ignore + const T = Type.Transform(Type.Null()) + .Decode(value => Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number() + })) + .Encode(value => null) + Expect(T).ToStatic() + Expect(T).ToStaticDecode< + TObject<{ + x: TNumber + y: TNumber + z: TNumber + }> + >() + Expect(T).ToStaticEncode() + type T = StaticDecode + type S = Static // type S = { + // x: number; + // y: number; + // z: number; + // } +} +{ + // ensure decode as optional + // prettier-ignore + const T = Type.Object({ + x: Type.Optional(Type.Number()), + y: Type.Optional(Type.Number()) + }) + Expect(T).ToStaticDecode<{ x?: number | undefined; y?: number | undefined }>() +} +{ + // ensure decode as readonly + // prettier-ignore + const T = Type.Object({ + x: Type.Readonly(Type.Number()), + y: Type.Readonly(Type.Number()) + }) + Expect(T).ToStaticDecode<{ readonly x: number; readonly y: number }>() +} +{ + // ensure decode as optional union + // prettier-ignore + const T = Type.Object({ + x: Type.Optional(Type.Union([ + Type.String(), + Type.Number() + ])) + }) + Expect(T).ToStaticDecode<{ x?: string | number | undefined }>() +} +{ + // should decode within generic function context + // https://github.com/sinclairzx81/typebox/issues/554 + // prettier-ignore + // const ArrayOrSingle = (schema: T) => + // Type.Transform(Type.Union([schema, Type.Array(schema)])[0]) + // .Decode((value) => (Array.isArray(value) ? value : [value])) + // .Encode((value) => (value.length === 1 ? value[0] : value) as Static[]); + // const T = ArrayOrSingle(Type.String()) + // Expect(T).ToStaticDecode() +} +{ + // should correctly decode record keys + // https://github.com/sinclairzx81/typebox/issues/555 + // prettier-ignore + const T = Type.Object({ + x: Type.Optional(Type.Record(Type.Number(), Type.String())) + }) + type A = StaticDecode + type Test = E extends A ? true : false + type E1 = Test + type E2 = Test + type E3 = Test + type E4 = Test + type E5 = Test + // assignment + const E1: E1 = true + const E2: E2 = true + const E3: E3 = true + const E4: E4 = false + const E5: E5 = true +} +{ + // should correctly decode array + // https://github.com/sinclairzx81/typebox/issues/561 + const T = Type.Object({ + x: Type.Array(Type.Object({ y: Type.String() })), + }) + Expect(T).ToStaticDecode<{ x: { y: string }[] }>() +} +{ + // should decode generic union + const GenericUnion = (t: T) => Type.Union([t, Type.Null()]) + const T = Type.Transform(Type.String()) + .Decode((value) => new Date(value)) + .Encode((value) => value.toISOString()) + Expect(T).ToStaticDecode() + Expect(GenericUnion(T)).ToStaticDecode() +} +{ + // should decode generic tuple + const GenericTuple = (t: T) => Type.Tuple([t, Type.Null()]) + const T = Type.Transform(Type.String()) + .Decode((value) => new Date(value)) + .Encode((value) => value.toISOString()) + Expect(T).ToStaticDecode() + Expect(GenericTuple(T)).ToStaticDecode<[Date, null]>() +} +{ + // should decode generic intersect + const GenericIntersect = (t: T) => Type.Intersect([t, Type.Literal(1)]) + const T = Type.Transform(Type.Number()) + .Decode((value) => value) + .Encode((value) => value) + Expect(T).ToStaticDecode() + Expect(GenericIntersect(T)).ToStaticDecode<1>() +} +{ + // should decode enum + enum E { + A, + B, + C, + } + const T = Type.Transform(Type.Enum(E)) + .Decode((value) => 1 as const) + .Encode((value) => E.A) + Expect(T).ToStaticDecode<1>() + Expect(T).ToStaticEncode() +} +{ + // should transform functions + const S = Type.Transform(Type.Number()) + .Decode((value) => new Date(value)) + .Encode((value) => value.getTime()) + const T = Type.Function([S], S) + Expect(T).ToStaticDecode<(x: Date) => Date>() + Expect(T).ToStaticEncode<(x: number) => number>() +} +{ + // should transform constructors + const S = Type.Transform(Type.Number()) + .Decode((value) => new Date(value)) + .Encode((value) => value.getTime()) + const T = Type.Constructor([S], S) + Expect(T).ToStaticDecode Date>() + Expect(T).ToStaticEncode number>() +} +// ------------------------------------------------------------- +// https://github.com/sinclairzx81/typebox/issues/798 +// ------------------------------------------------------------- +{ + const c1: TypeCheck = {} as any + const x1 = c1.Decode({}) + const x2 = Value.Decode({} as any, {}) +} +// ------------------------------------------------------------- +// https://github.com/sinclairzx81/typebox/issues/1178 +// ------------------------------------------------------------- +// immediate +{ + const T = Type.Module({ + A: Type.Transform(Type.String()) + .Decode((value) => parseInt(value)) + .Encode((value) => value.toString()), + }).Import('A') + Expect(T).ToStaticDecode() + Expect(T).ToStaticEncode() +} +// referential +{ + const T = Type.Module({ + A: Type.Transform(Type.String()) + .Decode((value) => parseInt(value)) + .Encode((value) => value.toString()), + B: Type.Ref('A'), + }).Import('B') + Expect(T).ToStaticDecode() + Expect(T).ToStaticEncode() +} +// deep-referential +{ + const T = Type.Module({ + A: Type.Transform(Type.String()) + .Decode((value) => parseInt(value)) + .Encode((value) => value.toString()), + B: Type.Ref('A'), + C: Type.Ref('B'), + D: Type.Ref('C'), + E: Type.Ref('D'), + }).Import('E') + Expect(T).ToStaticDecode() + Expect(T).ToStaticEncode() +} +// interior-transform referential +{ + const T = Type.Module({ + A: Type.String(), + B: Type.Ref('A'), + C: Type.Ref('B'), + T: Type.Transform(Type.Ref('C')) + .Decode((value) => parseInt(value as string)) + .Encode((value) => value.toString()), + X: Type.Ref('T'), + Y: Type.Ref('X'), + Z: Type.Ref('Y'), + }).Import('Z') + Expect(T).ToStaticDecode() + Expect(T).ToStaticEncode() +} diff --git a/test/static/tsconfig.json b/test/static/tsconfig.json new file mode 100644 index 000000000..6710ecd93 --- /dev/null +++ b/test/static/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "files": ["index.ts"] +} diff --git a/test/static/tuple.ts b/test/static/tuple.ts new file mode 100644 index 000000000..61fde9821 --- /dev/null +++ b/test/static/tuple.ts @@ -0,0 +1,10 @@ +import { Expect } from './assert' +import { Type, Static } from '@sinclair/typebox' + +{ + const T = Type.Tuple([Type.Number(), Type.String(), Type.Boolean()]) + + type T = Static + + Expect(T).ToStatic<[number, string, boolean]>() +} diff --git a/test/static/uncapitalize.ts b/test/static/uncapitalize.ts new file mode 100644 index 000000000..226504b94 --- /dev/null +++ b/test/static/uncapitalize.ts @@ -0,0 +1,14 @@ +import { Expect } from './assert' +import { Type } from '@sinclair/typebox' + +Expect(Type.Uncapitalize(Type.Literal('HELLO'))).ToStatic<'hELLO'>() + +Expect(Type.Uncapitalize(Type.Union([Type.Literal('HELLO'), Type.Literal('WORLD')]))).ToStatic<'hELLO' | 'wORLD'>() + +Expect(Type.Uncapitalize(Type.TemplateLiteral('HELLO${0|1}'))).ToStatic<'hELLO0' | 'hELLO1'>() + +// prettier-ignore +Expect(Type.Uncapitalize(Type.TemplateLiteral([Type.Literal('HELLO'), Type.Union([Type.Literal(1), Type.Literal(2)])]))).ToStatic<'hELLO1' | 'hELLO2'>() + +// passthrough +Expect(Type.Uncapitalize(Type.Object({ x: Type.Number() }))).ToStatic<{ x: number }>() diff --git a/test/static/union.ts b/test/static/union.ts new file mode 100644 index 000000000..3decc9e08 --- /dev/null +++ b/test/static/union.ts @@ -0,0 +1,106 @@ +import { Expect } from './assert' +import { Type, Static } from '@sinclair/typebox' + +{ + const A = Type.String() + const B = Type.Number() + const T = Type.Union([A, B]) + + type T = Static + + Expect(T).ToStatic() +} +{ + const A = Type.Object({ + A: Type.String(), + B: Type.String(), + }) + const B = Type.Object({ + X: Type.Number(), + Y: Type.Number(), + }) + const T = Type.Union([A, B]) + + type T = Static + + Expect(T).ToStatic< + | { + A: string + B: string + } + | { + X: number + Y: number + } + >() +} + +{ + const A = Type.Object({ + A: Type.String(), + B: Type.String(), + }) + const B = Type.Object({ + X: Type.Number(), + Y: Type.Number(), + }) + const T = Type.Union([A, B, Type.Intersect([A, B])]) + + type T = Static + + Expect(T).ToStatic< + | { + A: string + B: string + } + | { + X: number + Y: number + } + | ({ + A: string + B: string + } & { + X: number + Y: number + }) + >() +} + +{ + const T = Type.Union([]) + Expect(T).ToStaticNever() +} +// prettier-ignore +{ // Scalable Union + const X = Type.Object({ x: Type.Number() }) + const Y = Type.Object({ y: Type.Number() }) + const Z = Type.Object({ z: Type.Number() }) + const W = Type.Object({ w: Type.Number() }) + + const T = Type.Union([ + X, Y, Z, W, X, Y, Z, W, + X, Y, Z, W, X, Y, Z, W, + X, Y, Z, W, X, Y, Z, W, + X, Y, Z, W, X, Y, Z, W, + X, Y, Z, W, X, Y, Z, W, + X, Y, Z, W, X, Y, Z, W, + X, Y, Z, W, X, Y, Z, W, + X, Y, Z, W, X, Y, Z, W, + + X, Y, Z, W, X, Y, Z, W, + X, Y, Z, W, X, Y, Z, W, + X, Y, Z, W, X, Y, Z, W, + X, Y, Z, W, X, Y, Z, W, + X, Y, Z, W, X, Y, Z, W, + X, Y, Z, W, X, Y, Z, W, + X, Y, Z, W, X, Y, Z, W, + X, Y, Z, W, X, Y, Z, W, + ]) + Expect(T).ToStatic< + { x: number } | + { y: number } | + { z: number } | + { w: number } + >() +} diff --git a/test/static/unknown.ts b/test/static/unknown.ts new file mode 100644 index 000000000..0ff2e5d81 --- /dev/null +++ b/test/static/unknown.ts @@ -0,0 +1,4 @@ +import { Expect } from './assert' +import { Type } from '@sinclair/typebox' + +Expect(Type.Unknown()).ToStatic() diff --git a/test/static/uppercase.ts b/test/static/uppercase.ts new file mode 100644 index 000000000..2341feab6 --- /dev/null +++ b/test/static/uppercase.ts @@ -0,0 +1,14 @@ +import { Expect } from './assert' +import { Type } from '@sinclair/typebox' + +Expect(Type.Uppercase(Type.Literal('hello'))).ToStatic<'HELLO'>() + +Expect(Type.Uppercase(Type.Union([Type.Literal('hello'), Type.Literal('world')]))).ToStatic<'HELLO' | 'WORLD'>() + +Expect(Type.Uppercase(Type.TemplateLiteral('HELLO${0|1}'))).ToStatic<'HELLO0' | 'HELLO1'>() + +// prettier-ignore +Expect(Type.Uppercase(Type.TemplateLiteral([Type.Literal('hello'), Type.Union([Type.Literal(1), Type.Literal(2)])]))).ToStatic<'HELLO1' | 'HELLO2'>() + +// passthrough +Expect(Type.Uppercase(Type.Object({ x: Type.Number() }))).ToStatic<{ x: number }>() diff --git a/tsconfig.json b/tsconfig.json index 893bd6203..80620ddf5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,15 +1,19 @@ { "compilerOptions": { "strict": true, - "target": "ESNext", - "moduleResolution": "node", - "removeComments": true, - "lib": ["ESNext"], + "target": "ES2020", + "module": "Node16", + "moduleResolution": "Node16", "baseUrl": ".", "paths": { - "@sinclair/typebox": [ - "src/typebox.ts" - ] + "@sinclair/typebox/compiler": ["src/compiler/index.ts"], + "@sinclair/typebox/errors": ["src/errors/index.ts"], + "@sinclair/typebox/parser": ["src/parser/index.ts"], + "@sinclair/typebox/syntax": ["src/syntax/index.ts"], + "@sinclair/typebox/system": ["src/system/index.ts"], + "@sinclair/typebox/type": ["src/type/index.ts"], + "@sinclair/typebox/value": ["src/value/index.ts"], + "@sinclair/typebox": ["src/index.ts"], } } } \ No newline at end of file diff --git a/typebox.png b/typebox.png new file mode 100644 index 000000000..d38acd137 Binary files /dev/null and b/typebox.png differ