diff --git a/.changeset/fair-moons-study.md b/.changeset/fair-moons-study.md
new file mode 100644
index 00000000..0b6d89f7
--- /dev/null
+++ b/.changeset/fair-moons-study.md
@@ -0,0 +1,5 @@
+---
+"@traversable/json-schema": patch
+---
+
+init(json-schema): initializes `@traversable/json-schema`
diff --git a/.changeset/lazy-chefs-cry.md b/.changeset/lazy-chefs-cry.md
new file mode 100644
index 00000000..fcc897be
--- /dev/null
+++ b/.changeset/lazy-chefs-cry.md
@@ -0,0 +1,14 @@
+---
+"@traversable/schema-to-json-schema": patch
+"@traversable/json-schema-types": patch
+"@traversable/json-schema-test": patch
+"@traversable/typebox-test": patch
+"@traversable/json-schema": patch
+"@traversable/schema-seed": patch
+"@traversable/registry": patch
+"@traversable/zod-test": patch
+"@traversable/schema": patch
+"@traversable/zod": patch
+---
+
+feat(json-schema-types): adds fuzz-tested Json Schema validator via `JsonSchema.check`
diff --git a/.changeset/major-melons-smile.md b/.changeset/major-melons-smile.md
new file mode 100644
index 00000000..22572fd0
--- /dev/null
+++ b/.changeset/major-melons-smile.md
@@ -0,0 +1,5 @@
+---
+"@traversable/typebox-test": patch
+---
+
+init(typebox-test): initializes `@traversable/typebox-test`
diff --git a/.changeset/mighty-seas-film.md b/.changeset/mighty-seas-film.md
new file mode 100644
index 00000000..704cb104
--- /dev/null
+++ b/.changeset/mighty-seas-film.md
@@ -0,0 +1,5 @@
+---
+"@traversable/json-schema-types": patch
+---
+
+init(json-schema-types): initializes `@traversable/json-schema-types`
diff --git a/.changeset/orange-trees-heal.md b/.changeset/orange-trees-heal.md
new file mode 100644
index 00000000..14888d77
--- /dev/null
+++ b/.changeset/orange-trees-heal.md
@@ -0,0 +1,5 @@
+---
+"@traversable/json-schema-test": patch
+---
+
+init(json-schema-test): initializes `@traversable/json-schema-test`
diff --git a/.changeset/shaggy-actors-drum.md b/.changeset/shaggy-actors-drum.md
new file mode 100644
index 00000000..3a9c162e
--- /dev/null
+++ b/.changeset/shaggy-actors-drum.md
@@ -0,0 +1,18 @@
+---
+"@traversable/schema-to-json-schema": patch
+"@traversable/json-schema-types": patch
+"@traversable/json-schema-test": patch
+"@traversable/schema-compiler": patch
+"@traversable/schema-errors": patch
+"@traversable/typebox-test": patch
+"@traversable/json-schema": patch
+"@traversable/schema-seed": patch
+"@traversable/registry": patch
+"@traversable/zod-test": patch
+"@traversable/typebox": patch
+"@traversable/schema": patch
+"@traversable/json": patch
+"@traversable/zod": patch
+---
+
+feat(json-schema): adds fuzz-tested `JsonSchema.equals`
diff --git a/.changeset/stale-rocks-allow.md b/.changeset/stale-rocks-allow.md
new file mode 100644
index 00000000..b659e0a9
--- /dev/null
+++ b/.changeset/stale-rocks-allow.md
@@ -0,0 +1,5 @@
+---
+"@traversable/zod-test": patch
+---
+
+init(zod-test): initializes `@traversable/zod-test`
diff --git a/.changeset/tall-bikes-brush.md b/.changeset/tall-bikes-brush.md
new file mode 100644
index 00000000..aa29806a
--- /dev/null
+++ b/.changeset/tall-bikes-brush.md
@@ -0,0 +1,5 @@
+---
+"@traversable/zod-types": patch
+---
+
+init(zod-types): initializes `@traversable/zod-types`
diff --git a/README.md b/README.md
index 4b948536..228b4502 100644
--- a/README.md
+++ b/README.md
@@ -56,7 +56,7 @@ Let's look at a concrete example of how `@traversable/schema` can be used as a r
For this example, we'll be using [`@traversable/zod`](https://github.com/traversable/schema/tree/main/packages/zod), since zod is the library most users are familiar with.
-Let's write a function that takes an arbitrary zod schema as input an stringifies it.
+Let's write a function that takes an arbitrary zod schema as input and stringifies it.
> [!NOTE]
> This functionality is already available off-the shelf via `zx.toString`.
@@ -496,8 +496,11 @@ let toAPI = User.encode(fromAPI)
```mermaid
flowchart TD
+ json-schema(json-schema)
+ json-schema-test(json-schema-test)
registry(registry)
zod(zod)
+ zod-types(zod-types)
json(json) -.-> registry(registry)
schema(schema) -.-> registry(registry)
typebox-test(typebox-test) -.-> registry(registry)
@@ -510,6 +513,8 @@ flowchart TD
derive-validators(derive-validators) -.-> json(json)
derive-validators(derive-validators) -.-> registry(registry)
derive-validators(derive-validators) -.-> schema(schema)
+ json-schema-types(json-schema-types) -.-> json(json)
+ json-schema-types(json-schema-types) -.-> registry(registry)
schema-compiler(schema-compiler) -.-> json(json)
schema-compiler(schema-compiler) -.-> registry(registry)
schema-compiler(schema-compiler) -.-> schema(schema)
diff --git a/config/__generated__/package-list.ts b/config/__generated__/package-list.ts
index d916a3da..a0628e74 100644
--- a/config/__generated__/package-list.ts
+++ b/config/__generated__/package-list.ts
@@ -3,6 +3,9 @@ export const PACKAGES = [
"packages/derive-equals",
"packages/derive-validators",
"packages/json",
+ "packages/json-schema",
+ "packages/json-schema-test",
+ "packages/json-schema-types",
"packages/registry",
"packages/schema",
"packages/schema-compiler",
@@ -14,6 +17,7 @@ export const PACKAGES = [
"packages/typebox",
"packages/typebox-test",
"packages/zod",
- "packages/zod-test"
+ "packages/zod-test",
+ "packages/zod-types"
] as const
export type PACKAGES = typeof PACKAGES
\ No newline at end of file
diff --git a/packages/json-schema-test/README.md b/packages/json-schema-test/README.md
new file mode 100644
index 00000000..47fcad17
--- /dev/null
+++ b/packages/json-schema-test/README.md
@@ -0,0 +1,29 @@
+
+
แฏ๐๐ฟ๐ฎ๐๐ฒ๐ฟ๐๐ฎ๐ฏ๐น๐ฒ/๐ท๐๐ผ๐ป-๐๐ฐ๐ต๐ฒ๐บ๐ฎ-๐๐ฒ๐๐
+
+
+
+ Testing utility that generates arbitrary, pseudorandom Json Schema documents, powered by fast-check
+
+
+
+
+
+
+

+
+

+
+
+
+
diff --git a/packages/json-schema-test/package.json b/packages/json-schema-test/package.json
new file mode 100644
index 00000000..58d08bec
--- /dev/null
+++ b/packages/json-schema-test/package.json
@@ -0,0 +1,44 @@
+{
+ "name": "@traversable/json-schema-test",
+ "type": "module",
+ "version": "0.0.0",
+ "private": false,
+ "description": "",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/traversable/schema.git",
+ "directory": "packages/json-schema-test"
+ },
+ "bugs": {
+ "url": "https://github.com/traversable/schema/issues",
+ "email": "ahrjarrett@gmail.com"
+ },
+ "@traversable": {
+ "generateExports": { "include": ["**/*.ts"] },
+ "generateIndex": { "include": ["**/*.ts"] }
+ },
+ "publishConfig": {
+ "access": "public",
+ "directory": "dist",
+ "registry": "https://registry.npmjs.org"
+ },
+ "scripts": {
+ "bench": "echo NOTHING TO BENCH",
+ "build": "pnpm build:esm && pnpm build:cjs && pnpm build:annotate",
+ "build:annotate": "babel build --plugins annotate-pure-calls --out-dir build --source-maps",
+ "build:esm": "tsc -b tsconfig.build.json",
+ "build:cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps",
+ "check": "tsc -b tsconfig.json",
+ "clean": "pnpm run \"/^clean:.*/\"",
+ "clean:build": "rm -rf .tsbuildinfo dist build",
+ "clean:deps": "rm -rf node_modules",
+ "test": "vitest"
+ },
+ "dependencies": {
+ "@traversable/json": "workspace:^",
+ "@traversable/json-schema-types": "workspace:^",
+ "@traversable/registry": "workspace:^"
+ },
+ "devDependencies": {}
+}
diff --git a/packages/json-schema-test/src/__generated__/__manifest__.ts b/packages/json-schema-test/src/__generated__/__manifest__.ts
new file mode 100644
index 00000000..dc75c34e
--- /dev/null
+++ b/packages/json-schema-test/src/__generated__/__manifest__.ts
@@ -0,0 +1,44 @@
+export default {
+ "name": "@traversable/json-schema-test",
+ "type": "module",
+ "version": "0.0.0",
+ "private": false,
+ "description": "",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/traversable/schema.git",
+ "directory": "packages/json-schema-test"
+ },
+ "bugs": {
+ "url": "https://github.com/traversable/schema/issues",
+ "email": "ahrjarrett@gmail.com"
+ },
+ "@traversable": {
+ "generateExports": { "include": ["**/*.ts"] },
+ "generateIndex": { "include": ["**/*.ts"] }
+ },
+ "publishConfig": {
+ "access": "public",
+ "directory": "dist",
+ "registry": "https://registry.npmjs.org"
+ },
+ "scripts": {
+ "bench": "echo NOTHING TO BENCH",
+ "build": "pnpm build:esm && pnpm build:cjs && pnpm build:annotate",
+ "build:annotate": "babel build --plugins annotate-pure-calls --out-dir build --source-maps",
+ "build:esm": "tsc -b tsconfig.build.json",
+ "build:cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps",
+ "check": "tsc -b tsconfig.json",
+ "clean": "pnpm run \"/^clean:.*/\"",
+ "clean:build": "rm -rf .tsbuildinfo dist build",
+ "clean:deps": "rm -rf node_modules",
+ "test": "vitest"
+ },
+ "dependencies": {
+ "@traversable/json": "workspace:^",
+ "@traversable/json-schema-types": "workspace:^",
+ "@traversable/registry": "workspace:^"
+ },
+ "devDependencies": {}
+} as const
\ No newline at end of file
diff --git a/packages/json-schema-test/src/exports.ts b/packages/json-schema-test/src/exports.ts
new file mode 100644
index 00000000..e0882de4
--- /dev/null
+++ b/packages/json-schema-test/src/exports.ts
@@ -0,0 +1,19 @@
+export * from './version.js'
+
+export type Seed = import('./generator-seed.js').Seed
+export * as Seed from './generator-seed.js'
+
+export {
+ Builder,
+ SchemaGenerator,
+ SeedGenerator,
+ SeedValidDataGenerator,
+ SeedInvalidDataGenerator,
+ seedToValidDataGenerator,
+ seedToInvalidDataGenerator,
+ seedToSchema,
+ seedToValidData,
+ seedToInvalidData,
+ seedsThatPreventGeneratingValidData,
+ seedsThatPreventGeneratingInvalidData,
+} from './generator.js'
diff --git a/packages/json-schema-test/src/generator-bounds.ts b/packages/json-schema-test/src/generator-bounds.ts
new file mode 100644
index 00000000..76f58a88
--- /dev/null
+++ b/packages/json-schema-test/src/generator-bounds.ts
@@ -0,0 +1,189 @@
+import * as fc from 'fast-check'
+
+import type { newtype } from '@traversable/registry'
+import { fn, Number_isFinite, Number_isNatural, Number_isSafeInteger } from '@traversable/registry'
+
+/** @internal */
+const nullable = (model: fc.Arbitrary) => fc.oneof(fc.constant(null), fc.constant(null), model)
+
+/** @internal */
+const isBigInt = (x: unknown) => typeof x === 'bigint'
+
+export const defaultDoubleConstraints = {
+ noNaN: true,
+ noDefaultInfinity: true,
+} satisfies fc.DoubleConstraints
+
+const defaultIntBounds = [-0x1000, +0x1000, null] satisfies Bounds_int
+const defaultNumberBounds = [-0x10000, +0x10000, null, false, false] satisfies Bounds_number
+const defaultStringBounds = [0, +0x40] satisfies Bounds_string
+const defaultArrayBounds = [0, +0x10] satisfies Bounds_array
+
+export const defaults = {
+ integer: defaultIntBounds,
+ number: defaultNumberBounds,
+ string: defaultStringBounds,
+ array: defaultArrayBounds,
+}
+
+const clampMin
+ : (min: T, max: T, predicate: (x: T | null) => x is T) => (x: T | null, y: T | null) => T | null
+ = (min, max, predicate) => (x, y) => {
+ if (!predicate(x)) {
+ return null
+ } else if (!predicate(y)) {
+ return x < min ? min : max < x ? max : x
+ } else {
+ const z = x < y ? x : y
+ return z < min ? min : max < z ? max : z
+ }
+ }
+
+const clampMax
+ : (min: T, max: T, predicate: (x: T | null) => x is T) => (x: T | null, y: T | null) => T | null
+ = (min, max, predicate) => (x, y) => {
+ if (!predicate(x)) {
+ return null
+ } else if (!predicate(y)) {
+ return x < min ? min : max < x ? max : x
+ } else {
+ const z = x > y ? x : y
+ return z < min ? min : max < z ? max : z
+ }
+ }
+
+const clampIntMin = clampMin(defaults.integer[0], defaults.integer[1], Number_isSafeInteger)
+const clampIntMax = clampMax(defaults.integer[0], defaults.integer[1], Number_isSafeInteger)
+const clampNumberMin = clampMin(defaults.number[0], defaults.number[1], Number_isFinite)
+const clampNumberMax = clampMin(defaults.number[0], defaults.number[1], Number_isFinite)
+const clampStringMin = clampMin(defaults.string[0], defaults.string[1], Number_isNatural)
+const clampStringMax = clampMax(defaults.string[0], defaults.string[1], Number_isNatural)
+const clampArrayMin = clampMin(defaults.array[0], defaults.array[1], Number_isNatural)
+const clampArrayMax = clampMax(defaults.array[0], defaults.array[1], Number_isNatural)
+
+export const makeInclusiveBounds = (model: fc.Arbitrary) => ({ minimum: model, maximum: model })
+
+export { Bounds_int as int }
+interface Bounds_int extends newtype<[
+ minimum: number | null,
+ maximum: number | null,
+ multipleOf: number | null,
+]> {}
+
+const Bounds_int
+ : (model: fc.Arbitrary) => fc.Arbitrary
+ = (model) => fc.tuple(nullable(model), nullable(model), nullable(model)).map(([x, y, multipleOf]) => [
+ clampIntMin(x, y),
+ clampIntMax(y, x),
+ multipleOf,
+ // clampInt(multipleOf),
+ ])
+
+export { Bounds_string as string }
+interface Bounds_string extends newtype<[
+ minLength: number | null,
+ maxLength: number | null,
+]> {}
+
+const Bounds_string
+ : (model: fc.Arbitrary) => fc.Arbitrary
+ = (model) => fc.tuple(nullable(model), nullable(model), nullable(model)).map(
+ ([x, y, length]) => Number_isNatural(length)
+ ? [null, null] satisfies [any, any]
+ // [clampString(length), clampString(length)]
+ : [clampStringMin(x, y), clampStringMax(y, x)]
+ )
+
+export { Bounds_number as number }
+interface Bounds_number extends newtype<[
+ minimum: number | null,
+ maximum: number | null,
+ multipleOf: number | null,
+ exclusiveMinimum: boolean,
+ exclusiveMaximum: boolean,
+]> {}
+
+const deltaIsSubEpsilon = (x: number, y: number) => Math.abs(x - y) < Number.EPSILON
+
+const Bounds_number
+ : (model: fc.Arbitrary) => fc.Arbitrary
+ = (model) => fc.tuple(
+ nullable(model),
+ nullable(model),
+ nullable(model),
+ fc.boolean(),
+ fc.boolean()
+ ).map(
+ fn.flow(
+ ([x, y, multipleOf, minExcluded, maxExcluded]): Bounds_number => [
+ clampNumberMin(x, y),
+ clampNumberMax(y, x),
+ // clampNumber(multipleOf),
+ multipleOf,
+ minExcluded,
+ maxExcluded,
+ ],
+ ([min, max, multipleOf, minExcluded, maxExcluded]): Bounds_number => [
+ min,
+ ((minExcluded || maxExcluded) && min != null && max != null && deltaIsSubEpsilon(min, max)) ? max + 1 : max,
+ multipleOf,
+ minExcluded,
+ maxExcluded
+ ],
+ )
+ )
+
+export { Bounds_array as array }
+interface Bounds_array extends newtype<[
+ minLength: number | null,
+ maxLength: number | null,
+]> {}
+
+const Bounds_array
+ : (model: fc.Arbitrary) => fc.Arbitrary
+ = (model) => fc.tuple(
+ nullable(model),
+ nullable(model),
+ fc.constant(null)
+ ).map(([x, y, exactLength]) =>
+ Number_isNatural(exactLength)
+ ? [null, null]
+ : [clampArrayMin(x, y), clampArrayMax(y, x)]
+ )
+
+
+export const integerBoundsToIntegerConstraints
+ : (bounds?: Bounds_int) => fc.IntegerConstraints
+ = (bounds = defaultIntBounds) => {
+ const [min, max, multipleOf] = bounds
+ return {
+ max: max ?? void 0,
+ min: min ?? void 0,
+ } satisfies fc.IntegerConstraints
+ }
+
+export const numberBoundsToDoubleConstraints
+ : (bounds?: Bounds_number) => fc.DoubleConstraints
+ = (bounds = defaultNumberBounds) => {
+ const [min, max, multipleOf, minExcluded, maxExcluded] = bounds
+ return {
+ ...defaultDoubleConstraints, max: max ?? void 0,
+ min: min ?? void 0,
+ minExcluded,
+ maxExcluded,
+ } satisfies fc.DoubleConstraints
+ }
+
+export const stringBoundsToStringConstraints
+ : (bounds?: Bounds_string) => fc.StringConstraints
+ = ([minLength, maxLength] = defaultStringBounds) => ({
+ minLength: minLength ?? void 0,
+ maxLength: maxLength ?? void 0
+ }) satisfies fc.StringConstraints
+
+export const arrayBoundsToArrayConstraints
+ : (bounds?: Bounds_array) => fc.ArrayConstraints
+ = ([minLength, maxLength] = defaultArrayBounds) => ({
+ minLength: minLength ?? void 0,
+ maxLength: maxLength ?? void 0
+ }) satisfies fc.ArrayConstraints
diff --git a/packages/json-schema-test/src/generator-options.ts b/packages/json-schema-test/src/generator-options.ts
new file mode 100644
index 00000000..348c4849
--- /dev/null
+++ b/packages/json-schema-test/src/generator-options.ts
@@ -0,0 +1,333 @@
+import * as fc from 'fast-check'
+
+import type { SeedMap } from './generator.js'
+import * as Bounds from './generator-bounds.js'
+import { byTag } from './generator-seed.js'
+import { TypeNames } from '@traversable/json-schema-types'
+
+export type ArrayParams = {
+ minLength?: number
+ maxLength?: number
+}
+
+export type IntegerParams = {
+ minimum?: number
+ maximum?: number
+ multipleOf?: number
+}
+
+export type NumberParams = {
+ minimum?: number
+ maximum?: number
+ minExcluded?: boolean
+ maxExcluded?: boolean
+ multipleOf?: number
+}
+
+export type BigIntParams = {
+ minimum?: bigint
+ maximum?: bigint
+ multipleOf?: bigint
+}
+
+export type StringParams = {
+ /* prefix?: string, postfix?: string, pattern?: string, substring?: string, length?: number */
+ minLength?: number
+ maxLength?: number
+}
+
+export type Params = {
+ array?: ArrayParams
+ boolean?: {}
+ const?: {}
+ enum?: {}
+ integer?: IntegerParams
+ intersection?: {}
+ never?: {}
+ null?: {}
+ number?: NumberParams
+ object?: {}
+ record?: {}
+ string?: StringParams
+ tuple?: {}
+ union?: {}
+ unknown?: {}
+}
+
+export interface Options extends Partial>, Constraints {}
+
+export type InferConfigType = S extends Options ? T : never
+
+export interface OptionsBase<
+ T = never,
+ K extends
+ | string & keyof T
+ = string & keyof T
+> {
+ include: readonly K[]
+ exclude: readonly K[]
+ root: '*' | K
+ sortBias: { [K in keyof SeedMap]: number }
+ forceInvalid: boolean
+ minDepth: number
+ // minDepth: 1 | 2 | 3 | 4 | 5
+}
+export interface Config extends OptionsBase, byTypeName {}
+
+export type Constraints = {
+ array?: { minLength?: number, maxLength?: number }
+ boolean?: {}
+ const?: {}
+ enum?: {}
+ integer?: { min: undefined | number, max: undefined | number, multipleOf?: number } & fc.IntegerConstraints
+ intersection?: {}
+ never?: {}
+ null?: {}
+ number?: { min?: undefined | number, max?: undefined | number, multipleOf?: number } & fc.DoubleConstraints
+ object?: ObjectConstraints
+ record?: RecordConstraints
+ string?: fc.StringConstraints
+ tuple?: fc.ArrayConstraints
+ union?: fc.ArrayConstraints
+ unknown?: {}
+ ['*']?: fc.OneOfConstraints
+}
+
+export interface byTypeName extends Required> {
+ object: fc.UniqueArrayConstraintsRecommended<[k: string, v: unknown], string>
+ array: fc.IntegerConstraints
+}
+
+export type RecordConstraints = fc.DictionaryConstraints & {
+ additionalPropertiesOnly?: boolean
+ patternPropertiesOnly?: boolean
+}
+
+export type ObjectConstraints =
+ & Omit, 'minLength' | 'maxLength'>
+ & {
+ minKeys?: number
+ maxKeys?: number
+ size?: fc.SizeForArbitrary
+ }
+
+const objectDefaults = {
+ minKeys: 1,
+ maxKeys: 3,
+ size: 'xsmall',
+ selector: ([k]) => k,
+ comparator: 'SameValueZero',
+ depthIdentifier: fc.createDepthIdentifier(),
+} satisfies ObjectConstraints
+
+export const defaultConstraints = {
+ object: objectDefaults,
+ array: {
+ minLength: 0,
+ maxLength: 0x10
+ },
+ boolean: {},
+ const: {},
+ enum: {},
+ integer: {
+ min: undefined,
+ max: undefined,
+ multipleOf: Number.NaN,
+ },
+ intersection: {},
+ never: {},
+ null: {},
+ number: {
+ min: -0x10000,
+ max: 0x10000,
+ multipleOf: Number.NaN,
+ noNaN: true,
+ noDefaultInfinity: true,
+ minExcluded: false,
+ maxExcluded: false,
+ noInteger: false,
+ },
+ record: {
+ additionalPropertiesOnly: false,
+ patternPropertiesOnly: false,
+ depthIdentifier: fc.createDepthIdentifier(),
+ maxKeys: 3,
+ minKeys: 1,
+ noNullPrototype: false,
+ size: 'xsmall',
+ } satisfies RecordConstraints,
+ string: {
+ minLength: Bounds.defaults.string[0],
+ maxLength: Bounds.defaults.string[1],
+ size: 'xsmall',
+ unit: 'grapheme-ascii',
+ } satisfies fc.StringConstraints,
+ tuple: {
+ minLength: 1,
+ maxLength: 3,
+ size: 'xsmall',
+ depthIdentifier: fc.createDepthIdentifier(),
+ } satisfies fc.ArrayConstraints,
+ union: {
+ depthIdentifier: fc.createDepthIdentifier(),
+ minLength: 1,
+ maxLength: 3,
+ size: 'xsmall',
+ } satisfies fc.ArrayConstraints,
+ unknown: {},
+ ['*']: {
+ maxDepth: 3,
+ depthIdentifier: fc.createDepthIdentifier(),
+ depthSize: 'xsmall',
+ withCrossShrink: true,
+ } satisfies fc.OneOfConstraints,
+} as const satisfies { [K in keyof Constraints]-?: Required }
+
+export const defaults = {
+ exclude: [],
+ forceInvalid: false,
+ include: TypeNames,
+ minDepth: -1,
+ root: '*',
+ sortBias: byTag,
+} as const satisfies OptionsBase
+
+export function parseOptions(options?: Opts): Config>
+export function parseOptions(options?: Options): Config
+export function parseOptions(options: Options = defaults as never): Config {
+ const {
+ exclude = defaults.exclude,
+ forceInvalid = defaults.forceInvalid,
+ include = defaults.include,
+ minDepth: rootMinDepth = defaults.minDepth,
+ root = defaults.root,
+ sortBias = defaults.sortBias,
+ ['*']: {
+ maxDepth: starMaxDepth = defaultConstraints['*'].maxDepth,
+ depthSize: starDepthSize = defaultConstraints['*'].depthSize,
+ ...STAR
+ } = defaultConstraints['*'],
+ array: {
+ maxLength: arrayMax = defaultConstraints.array.maxLength,
+ minLength: arrayMin = defaultConstraints.array.minLength,
+ ...ARRAY
+ } = defaultConstraints.array,
+ boolean = defaultConstraints.boolean,
+ const: const_ = defaultConstraints.const,
+ enum: enum_ = defaultConstraints.enum,
+ integer: {
+ max: intMax,
+ min: intMin,
+ // ...INT
+ } = defaultConstraints.integer,
+ intersection = defaultConstraints.intersection,
+ never = defaultConstraints.never,
+ null: null_ = defaultConstraints.null,
+ number: {
+ max: numberMax,
+ maxExcluded: numberMaxExcluded,
+ min: numberMin,
+ minExcluded: numberMinExcluded,
+ // ...NUMBER
+ } = defaultConstraints.number,
+ record: {
+ maxKeys: recordMaxKeys = defaultConstraints.record.maxKeys,
+ minKeys: recordMinKeys = defaultConstraints.record.minKeys,
+ size: recordSize = defaultConstraints.record.size,
+ ...RECORD
+ } = defaultConstraints.record,
+ string: {
+ minLength: stringMinLength,
+ maxLength: stringMaxLength,
+ size: stringSize = defaultConstraints.string.size,
+ // ...STRING
+ } = defaultConstraints.string,
+ tuple: {
+ maxLength: tupleMaxLength = defaultConstraints.tuple.maxLength,
+ minLength: tupleMinLength = defaultConstraints.tuple.minLength,
+ ...TUPLE
+ } = defaultConstraints.tuple,
+ union: {
+ minLength: unionMinLength = defaultConstraints.union.minLength,
+ maxLength: unionMaxLength = defaultConstraints.union.maxLength,
+ size: unionSize = defaultConstraints.union.size,
+ ...UNION
+ } = defaultConstraints.union,
+ unknown = defaultConstraints.unknown,
+ object: {
+ maxKeys: objectMaxKeys = defaultConstraints.object.maxKeys,
+ minKeys: objectMinKeys = defaultConstraints.object.minKeys,
+ size: objectSize = defaultConstraints.object.size,
+ ...OBJECT
+ } = {
+ maxKeys: defaultConstraints.object.maxKeys,
+ minKeys: defaultConstraints.object.minKeys,
+ size: defaultConstraints.object.size,
+ },
+ } = options
+ return {
+ exclude: exclude,
+ forceInvalid,
+ include: include.length === 0 || include[0] === '*' ? defaults.include : include,
+ minDepth: rootMinDepth,
+ root,
+ sortBias: { ...defaults.sortBias, ...sortBias },
+ ['*']: {
+ ...STAR,
+ depthSize: starDepthSize,
+ maxDepth: starMaxDepth,
+ },
+ object: {
+ ...OBJECT,
+ minLength: objectMinKeys,
+ maxLength: objectMaxKeys,
+ size: objectSize,
+ },
+ array: {
+ ...ARRAY,
+ min: arrayMin,
+ max: arrayMax,
+ },
+ boolean,
+ integer: {
+ // ...INT,
+ max: intMax,
+ min: intMin,
+ },
+ intersection,
+ const: const_,
+ enum: enum_,
+ never,
+ null: null_,
+ number: {
+ max: numberMax,
+ min: numberMin,
+ maxExcluded: numberMaxExcluded,
+ minExcluded: numberMinExcluded,
+ },
+ record: {
+ ...RECORD,
+ maxKeys: recordMaxKeys,
+ minKeys: recordMinKeys,
+ size: recordSize,
+ },
+ string: {
+ // ...STRING,
+ minLength: stringMinLength,
+ maxLength: stringMaxLength,
+ size: stringSize,
+ },
+ tuple: {
+ ...TUPLE,
+ minLength: tupleMinLength,
+ maxLength: tupleMaxLength,
+ },
+ union: {
+ ...UNION,
+ minLength: unionMinLength,
+ maxLength: unionMaxLength,
+ size: unionSize,
+ },
+ unknown,
+ }
+}
diff --git a/packages/json-schema-test/src/generator-seed.ts b/packages/json-schema-test/src/generator-seed.ts
new file mode 100644
index 00000000..591992ca
--- /dev/null
+++ b/packages/json-schema-test/src/generator-seed.ts
@@ -0,0 +1,195 @@
+import type * as T from '@traversable/registry'
+import type { newtype } from '@traversable/registry'
+import { fn, Object_keys } from '@traversable/registry'
+
+import * as Bounds from './generator-bounds.js'
+import { TypeName } from '@traversable/json-schema-types'
+import { Json } from '@traversable/json'
+
+export type Tag = byTag[keyof byTag]
+export type byTag = typeof byTag
+export const byTag = {
+ boolean: 15 as const,
+ never: 35 as const,
+ null: 40 as const,
+ unknown: 55 as const,
+ integer: 100 as const,
+ number: 200 as const,
+ string: 250 as const,
+ enum: 350 as const,
+ const: 550 as const,
+ array: 1000 as const,
+ intersection: 6000 as const,
+ record: 7000 as const,
+ object: 7500 as const,
+ tuple: 8000 as const,
+ union: 8500 as const,
+} as const satisfies Record
+
+export function invert>(x: { [K in keyof T]: T[K] }): { [K in keyof T as T[K]]: K }
+export function invert(x: {}) {
+ return Object_keys(x).reduce((acc, k) => (acc[x[k]] = k, acc), {})
+}
+
+export type bySeed = typeof bySeed
+export const bySeed = invert(byTag)
+
+export type Seed =
+ & Seed.TerminalMap
+ & Seed.BoundableMap
+ & Seed.ValueMap
+ & Seed.UnaryMap
+
+export declare namespace Seed {
+ type Fixpoint = [
+ tag: Tag, children?:
+ unknown,
+ bounds?:
+ | Seed.Array[2]
+ | Seed.Object[2]
+ | Seed.Record[2]
+ ]
+ type F =
+ | Seed.Nullary
+ | Seed.Unary
+ type Nullary =
+ | Seed.Terminal
+ | Seed.Boundable
+ | Seed.Value
+ type Unary =
+ | Seed.Array
+ | Seed.Record
+ | Seed.Object
+ | Seed.Tuple
+ | Seed.Union
+ | Seed.Intersection
+
+ interface Free extends T.HKT { [-1]: Seed.F }
+ ////////////////
+ /// nullary
+ interface Boolean extends newtype<[byTag['boolean']]> {}
+ interface Never extends newtype<[byTag['never']]> {}
+ interface Null extends newtype<[byTag['null']]> {}
+ interface Unknown extends newtype<[byTag['unknown']]> {}
+ type Terminal = TerminalMap[keyof TerminalMap]
+ type TerminalMap = {
+ boolean: Boolean
+ never: Never
+ null: Null
+ unknown: Unknown
+ }
+ ////////////////
+ /// boundable
+ interface Integer extends newtype<[seed: byTag['integer'], bounds?: Bounds.int]> {}
+ interface Number extends newtype<[seed: byTag['number'], bounds?: Bounds.number]> {}
+ interface String extends newtype<[seed: byTag['string'], bounds?: Bounds.string]> {}
+ type Boundable = BoundableMap[keyof BoundableMap]
+ type BoundableMap = {
+ integer: Integer
+ number: Number
+ string: String
+ }
+ ////////////////
+ /// value
+ interface Const extends newtype<[seed: byTag['const'], value: Json]> {}
+ interface Enum extends newtype<[seed: byTag['enum'], value: Exclude[]]> {}
+ type Value = ValueMap[keyof ValueMap]
+ type ValueMap = {
+ const: Const
+ enum: Enum
+ }
+ ////////////////
+ /// unary
+ interface Array extends newtype<[seed: byTag['array'], def: T, bounds?: Bounds.array]> {}
+ type UnaryMap = {
+ array: Seed.Array
+ record: Seed.Record
+ object: Seed.Object
+ tuple: Seed.Tuple
+ union: Seed.Union
+ intersection: Seed.Intersection
+ }
+ type Composite =
+ | Seed.Array
+ | Seed.Record
+ | Seed.Object
+ | Seed.Tuple
+ type fromComposite = {
+ [byTag.array]: unknown[]
+ [byTag.record]: globalThis.Record
+ [byTag.object]: { [x: string]: unknown }
+ [byTag.tuple]: unknown[]
+ }
+ ////////////////
+ /// applicative
+ interface Union extends newtype<[seed: byTag['union'], def: T[]]> {}
+ interface Tuple extends newtype<[seed: byTag['tuple'], def: T[]]> {}
+ interface Object extends newtype<[seed: byTag['object'], def: [K: string, V: T][], req: string[]]> {}
+ ////////////////
+ /// binary
+ interface Record extends newtype<[seed: byTag['record'], additionalProperties?: T, patternProperties?: [K: string, V: T]]> {}
+ interface Intersection extends newtype<[seed: byTag['intersection'], def: T[]]> {}
+}
+
+export const Functor: T.Functor.Ix> = {
+ map(f) {
+ return (x) => {
+ switch (true) {
+ default: return x satisfies never
+ case x[0] === byTag.boolean: return x
+ case x[0] === byTag.never: return x
+ case x[0] === byTag.null: return x
+ case x[0] === byTag.unknown: return x
+ case x[0] === byTag.integer: return x
+ case x[0] === byTag.number: return x
+ case x[0] === byTag.string: return x
+ case x[0] === byTag.enum: return x
+ case x[0] === byTag.const: return x
+ case x[0] === byTag.array: return [x[0], f(x[1]), x[2]]
+ case x[0] === byTag.intersection: return [x[0], x[1].map(f)]
+ case x[0] === byTag.tuple: return [x[0], x[1].map(f)]
+ case x[0] === byTag.union: return [x[0], x[1].map(f)]
+ case x[0] === byTag.object: return [x[0], x[1].map(([k, v]) => [k, f(v)] satisfies [any, any]), x[2]]
+ case x[0] === byTag.record: {
+ return x[1] && x[2]
+ ? [x[0], f(x[1]), [x[2][0], f(x[2][1])] satisfies [any, any]]
+ : x[2] ? [x[0], undefined, [x[2][0], f(x[2][1])] satisfies [any, any]]
+ : x[1]
+ ? [x[0], f(x[1])] satisfies [any, any]
+ : [x[0]] satisfies [any]
+ }
+ }
+ }
+ },
+ mapWithIndex(f) {
+ return (x, isProperty) => {
+ switch (true) {
+ default: return x satisfies never
+ case x[0] === byTag.boolean: return x
+ case x[0] === byTag.never: return x
+ case x[0] === byTag.null: return x
+ case x[0] === byTag.unknown: return x
+ case x[0] === byTag.integer: return x
+ case x[0] === byTag.number: return x
+ case x[0] === byTag.string: return x
+ case x[0] === byTag.enum: return x
+ case x[0] === byTag.const: return x
+ case x[0] === byTag.array: return [x[0], f(x[1], false, x), x[2]]
+ case x[0] === byTag.tuple: return [x[0], x[1].map((_) => f(_, false, x))]
+ case x[0] === byTag.union: return [x[0], x[1].map((_) => f(_, isProperty, x))]
+ case x[0] === byTag.intersection: return [x[0], x[1].map((_) => f(_, isProperty, x))]
+ case x[0] === byTag.record: return x[1] && x[2]
+ ? [x[0], f(x[1], false, x), [x[2][0], f(x[2][1], false, x)] satisfies [any, any]]
+ : x[2] ? [x[0], undefined, [x[2][0], f(x[2][1], false, x)] satisfies [any, any]]
+ : x[1]
+ ? [x[0], f(x[1], false, x)]
+ : [x[0]]
+ case x[0] === byTag.object: return [x[0], x[1].map(([k, v]) => [k, f(v, true, x)] satisfies [any, any]), x[2]]
+ }
+ }
+ }
+}
+
+export const fold
+ : (g: (src: Seed.F, ix: boolean, x: Seed.Fixpoint) => T) => (src: Seed.F, isProperty?: boolean) => T
+ = (g) => (src, isProperty = false) => fn.catamorphism(Functor, false)(g)(src, isProperty)
diff --git a/packages/json-schema-test/src/generator.ts b/packages/json-schema-test/src/generator.ts
new file mode 100644
index 00000000..1a642ae3
--- /dev/null
+++ b/packages/json-schema-test/src/generator.ts
@@ -0,0 +1,560 @@
+import * as fc from 'fast-check'
+
+import type { newtype, inline } from '@traversable/registry'
+import {
+ Array_isArray,
+ fn,
+ isKeyOf,
+ isObject,
+ getRandomElementOf,
+ mutateRandomElementOf,
+ mutateRandomValueOf,
+ Number_isFinite,
+ Number_isNatural,
+ Number_isSafeInteger,
+ Object_assign,
+ Object_entries,
+ Object_fromEntries,
+ Object_keys,
+ omit,
+ pair,
+ pick,
+ symbol,
+ Object_create,
+ PATTERN,
+ escapeRegExp,
+} from '@traversable/registry'
+import type { TypeName } from '@traversable/json-schema-types'
+import { JsonSchema } from '@traversable/json-schema-types'
+
+type Config = import('./generator-options.js').Config
+import * as Config from './generator-options.js'
+import * as Bounds from './generator-bounds.js'
+import type { Tag } from './generator-seed.js'
+import { byTag, bySeed, Seed, fold } from './generator-seed.js'
+
+const identifier = fc.stringMatching(new RegExp(PATTERN.identifier, 'u'))
+const pattern = identifier.map((ident) => ident.replaceAll('$', ''))
+
+const literalValue = fc.oneof(
+ fc.string({ minLength: Bounds.defaults.string[0], maxLength: Bounds.defaults.string[1] }),
+ // identifier,
+ fc.double({ min: Bounds.defaults.number[0], max: Bounds.defaults.number[1], noNaN: true }),
+ fc.boolean(),
+)
+
+/**
+ * TODO: break this out into a proper json builder
+ */
+const jsonValue = fc.oneof(
+ literalValue,
+ fc.array(literalValue),
+ fc.dictionary(identifier, literalValue)
+)
+
+const TerminalMap = {
+ boolean: fn.const(fc.tuple(fc.constant(byTag.boolean))),
+ never: fn.const(fc.tuple(fc.constant(byTag.never))),
+ null: fn.const(fc.tuple(fc.constant(byTag.null))),
+ unknown: fn.const(fc.tuple(fc.constant(byTag.unknown))),
+} satisfies { [K in keyof Seed.TerminalMap]: SeedBuilder }
+
+const integerBounds = Bounds.int(fc.integer())
+const numberBounds = Bounds.number(fc.double())
+const stringBounds = Bounds.string(fc.nat())
+
+const BoundableMap = {
+ integer: fn.const(fc.tuple(fc.constant(byTag.integer), integerBounds)),
+ number: fn.const(fc.tuple(fc.constant(byTag.number), numberBounds)),
+ string: fn.const(fc.tuple(fc.constant(byTag.string), stringBounds)),
+} satisfies { [K in keyof Seed.BoundableMap]: SeedBuilder }
+
+const ValueMap = {
+ const: fn.const(fc.tuple(fc.constant(byTag.const), jsonValue)),
+ enum: fn.const(fc.tuple(fc.constant(byTag.enum), fc.array(literalValue, { minLength: 1 }))),
+} satisfies { [K in keyof Seed.ValueMap]: SeedBuilder }
+
+const UnaryMap = {
+ array: (tie) => fc.tuple(fc.constant(byTag.array), tie('*'), Bounds.array(fc.nat())),
+ tuple: (tie, $) => fc.tuple(fc.constant(byTag.tuple), fc.array(tie('*'), $)),
+ union: (tie, $) => fc.tuple(fc.constant(byTag.union), fc.array(tie('*'), $)),
+ record: (tie, $) => fc.tuple(
+ fc.constant(byTag.record),
+ tie('*'),
+ fc.tuple(pattern, tie('*')),
+ fc.constantFrom(1, 2, 3),
+ ).map(([type, additionalProperties, patternProperties, switcher]) =>
+ $.additionalPropertiesOnly ? [type, additionalProperties, undefined]
+ : $.patternPropertiesOnly ? [type, undefined, patternProperties]
+ : switcher === 1 ? [type, additionalProperties, patternProperties]
+ : switcher === 2 ? [type, undefined, patternProperties]
+ : switcher === 3 ? [type, additionalProperties, undefined]
+ : switcher satisfies never
+ ),
+ object: (tie, $) => fc.tuple(
+ fc.constant(byTag.object),
+ fc.uniqueArray(fc.tuple(identifier, tie('*')), $),
+ fc.nat(),
+ ).map(([type, xs, ix]) => {
+ const required = xs.slice(0, ix % xs.length).map(([k]) => k)
+ return [
+ type,
+ xs,
+ required,
+ ] satisfies [any, any, any]
+ }),
+ intersection: (tie) => entries(tie('*'), { minLength: 2 }).map(fn.flow(
+ (xs) => pair(
+ xs.slice(0, Math.ceil(xs.length / 2)),
+ xs.slice(Math.ceil(xs.length / 2)),
+ ),
+ ([l, r]) => pair(
+ pair(byTag.object, l),
+ pair(byTag.object, r),
+ ),
+ (both) => pair(byTag.intersection, both),
+ )),
+} satisfies { [K in keyof Seed.UnaryMap]: SeedBuilder }
+
+const TerminalSeeds = fn.map(Object_keys(TerminalMap), (tag) => byTag[tag])
+const BoundableSeeds = fn.map(Object_keys(BoundableMap), (tag) => byTag[tag])
+
+export interface SeedBuilder {
+ (tie: fc.LetrecTypedTie, $: Config.byTypeName[K]): fc.Arbitrary
+}
+
+export interface SeedMap extends newtype<{ [K in keyof Seed]: SeedBuilder }> {}
+export const SeedMap = {
+ ...TerminalMap,
+ ...BoundableMap,
+ ...ValueMap,
+ ...UnaryMap,
+} satisfies SeedMap
+
+export function isTerminal(x: unknown): x is Seed.Terminal | Seed.Boundable
+export function isTerminal(x: unknown) {
+ if (!Array_isArray(x)) return false
+ else {
+ const tag = x[0] as never
+ return TerminalSeeds.includes(tag) || BoundableSeeds.includes(tag)
+ }
+}
+
+export const pickAndSortNodes
+ : (nodes: readonly ([keyof SeedMap, unknown])[]) => ($: Config) => (keyof SeedMap)[]
+ = (nodes) => ({ include, exclude, sortBias } = Config.defaults as never) =>
+ nodes
+ .map(([k]) => k)
+ .filter((x) =>
+ (include ? include.includes(x as never) : true) &&
+ (exclude ? !exclude.includes(x as never) : true)
+ )
+ .sort((l, r) => sortBias[l] < sortBias[r] ? -1 : sortBias[l] > sortBias[r] ? 1 : 0)
+
+export function JsonSchema_Integer(bounds: Bounds.int = Bounds.defaults.integer): JsonSchema.Integer {
+ const [min, max /*, multipleOf */] = bounds
+ let schema: JsonSchema.Integer = { type: 'integer' as const }
+ if (Number_isSafeInteger(min)) schema.minimum = min
+ if (Number_isSafeInteger(max)) schema.maximum = max
+ // if (Number_isSafeInteger(multipleOf)) schema.multipleOf = multipleOf
+ return schema
+}
+
+export function JsonSchema_Number(bounds: Bounds.number = Bounds.defaults.number): JsonSchema.Number {
+ const [min, max, /* multipleOf */, minExcluded, maxExcluded] = bounds
+ let schema: JsonSchema.Number = { type: 'number' }
+ if (Number_isFinite(min)) {
+ if (minExcluded) schema.exclusiveMinimum = min
+ else schema.minimum = min
+ }
+ if (Number_isFinite(max)) {
+ if (maxExcluded) schema.exclusiveMaximum = max
+ else schema.maximum = max
+ }
+ return schema
+}
+
+export function JsonSchema_String(bounds: Bounds.string = Bounds.defaults.string): JsonSchema.String {
+ const [min, max, exactLength] = bounds
+ let schema: JsonSchema.String = { type: 'string' }
+ if (Number_isNatural(exactLength)) {
+ schema.minLength = exactLength
+ schema.maxLength = exactLength
+ return schema
+ } else {
+ if (Number_isNatural(min)) schema.minLength = min
+ if (Number_isNatural(max)) schema.maxLength = max
+ return schema
+ }
+}
+
+export function JsonSchema_Array(
+ items: T, bounds: Bounds.array = Bounds.defaults.array
+): JsonSchema.Array {
+ const [min, max] = bounds
+ let schema: JsonSchema.Array = { type: 'array', items }
+ if (Number_isNatural(min)) schema.minItems = min
+ if (Number_isNatural(max)) schema.maxItems = max
+ return schema
+}
+
+const branchNames = [
+ 'array',
+ 'object',
+ 'record',
+ 'tuple',
+] as const satisfies TypeName[]
+
+export interface Builder extends inline<{ [K in Tag]+?: fc.Arbitrary }> {
+ root?: fc.Arbitrary
+ invalid?: fc.Arbitrary
+ ['*']: fc.Arbitrary
+}
+
+export function Builder(base: Gen.Base):
+ >(
+ options?: Options,
+ overrides?: Partial>
+ ) => (tie: fc.LetrecLooselyTypedTie) => Builder
+
+export function Builder(base: Gen.Base) {
+ return >(options?: Options, overrides?: Partial>) => {
+ const $ = Config.parseOptions(options)
+ return (tie: fc.LetrecLooselyTypedTie) => {
+ const builder: { [x: string]: fc.Arbitrary } = fn.pipe(
+ { ...base, ...overrides },
+ (x) => pick(x, $.include),
+ (x) => omit(x, $.exclude),
+ (x) => fn.map(x, (f, k) => f(tie, $[k as never])),
+ )
+ const nodes = pickAndSortNodes(Object_entries(builder) as [k: keyof SeedMap, unknown][])($)
+ builder['*'] = fc.oneof($['*'], ...nodes.map((k) => builder[k]))
+ const root = isKeyOf(builder, $.root) && builder[$.root]
+ let leaf = builder['*']
+
+ if ($.minDepth > 0) {
+ let branchName = getRandomElementOf(branchNames)
+ do {
+ if (branchName === 'object') leaf = fc.tuple(fc.constant(byTag.object), entries(builder['*']))
+ else if (branchName === 'tuple') leaf = fc.tuple(fc.constant(byTag.tuple), fc.array(builder['*']))
+ } while (--$.minDepth > 0)
+ }
+
+ return Object_assign(
+ builder, {
+ ...root && { root },
+ ['*']: leaf
+ })
+ }
+ }
+}
+
+export declare namespace Gen {
+ type Base = { [K in keyof T]: (tie: fc.LetrecLooselyTypedTie, constraints: $[K & keyof $]) => fc.Arbitrary }
+ type Values = never | T[Exclude]
+ type InferArb = S extends fc.Arbitrary ? T : never
+ interface Builder extends newtype { ['*']: fc.Arbitrary>> }
+ type BuildBuilder, Out extends {} = BuilderBase> = never | Builder
+ type BuilderBase, $ extends ParseOptions = ParseOptions> = never |
+ & ([$['root']] extends [never] ? unknown : { root: fc.Arbitrary<$['root']> })
+ & { [K in Exclude<$['include'], $['exclude']>]: fc.Arbitrary }
+ type ParseOptions> = never | {
+ include: Options['include'] extends readonly unknown[] ? Options['include'][number] : keyof T
+ exclude: Options['exclude'] extends readonly unknown[] ? Options['exclude'][number] : never
+ root: Options['root'] extends keyof T ? T[Options['root']] : never
+ }
+}
+
+/**
+ * ## {@link Gen `Gen`}
+ */
+export function Gen(base: Gen.Base):
+ >(
+ options?: Options,
+ overrides?: Partial>
+ ) => Gen.BuildBuilder
+
+export function Gen(base: Gen.Base) {
+ return >(
+ options?: Options,
+ overrides?: Partial>
+ ): Builder => {
+ return fc.letrec(Builder(base)(options, overrides))
+ }
+}
+
+function entries(model: fc.Arbitrary, options?: fc.UniqueArrayConstraintsRecommended<[k: string, T], unknown>) {
+ return fc.uniqueArray(
+ fc.tuple(identifier, model),
+ { selector: options?.selector || (([k]) => k), ...options },
+ )
+}
+
+function intersect(x: unknown, y: unknown) {
+ return !isObject(x) ? y : !isObject(y) ? x : Object_assign(x, y)
+}
+
+const keyFromPattern = (key: string) => {
+ if (key.startsWith('^')) key = key.slice(1)
+ if (key.endsWith('$')) key = key.slice(0, -1)
+ return fc.sample(fc.stringMatching(new RegExp(`^${escapeRegExp(key)}$`)), 1)[0]
+}
+
+const GeneratorByTag = {
+ boolean: () => fc.boolean(),
+ never: () => fc.constant(void 0 as never),
+ null: () => fc.constant(null),
+ unknown: () => fc.anything(),
+ integer: (x) => fc.integer(Bounds.integerBoundsToIntegerConstraints(x[1])),
+ number: (x) => fc.double(Bounds.numberBoundsToDoubleConstraints(x[1])),
+ string: (x) => fc.string(Bounds.stringBoundsToStringConstraints(x[1])),
+ const: (x) => fc.constant(x[1]),
+ enum: (x) => fc.constant(x[1][0]),
+ array: (x) => fc.array(x[1], Bounds.arrayBoundsToArrayConstraints(x[2])),
+ record: (x) =>
+ x[1] && x[2] ? fc.tuple(
+ fc.dictionary(fc.string(), x[1]),
+ fc.record({ [keyFromPattern(x[2][0])]: x[2][1] })
+ ).map(([l, r]) => ({ ...l, ...r }))
+ : x[1] ? fc.dictionary(fc.string(), x[1])
+ : x[2] ? fc.record({ [keyFromPattern(x[2][0])]: x[2][1] })
+ : fc.constant({})
+ ,
+ tuple: (x) => fc.tuple(...x[1]),
+ union: (x) => fc.oneof(...(x[1] || [fc.constant(void 0 as never)])),
+ object: (x) => fc.record(Object_fromEntries(x[1]), { requiredKeys: x[2] }),
+ // object: (x) => fc.record(Object_fromEntries(x[1]), x[2].length === 0 ? {} : { requiredKeys: x[2] }),
+ intersection: (x) => fc.tuple(...x[1]).map((xs) => xs.reduce((x, y) => intersect(x, y), Object_create(null))),
+} satisfies {
+ [K in keyof Seed]: (x: Seed>[K], $: Config) => fc.Arbitrary
+}
+
+/**
+ * ## {@link seedToValidDataGenerator `seedToValidDataGenerator`}
+ *
+ * Convert a seed into an valid data generator.
+ *
+ * Valid in this context means that it will always satisfy the zod schema that the seed produces.
+ *
+ * To use it, you'll need to have [fast-check](https://github.com/dubzzz/fast-check) installed.
+ *
+ * To convert a seed to a zod schema, use {@link seedToSchema `seedToSchema`}.
+ *
+ * To convert a seed to an _invalid_ data generator, use {@link seedToInvalidDataGenerator `seedToInvalidDataGenerator`}.
+ */
+export function seedToValidDataGenerator(seed: Seed.F): fc.Arbitrary
+export function seedToValidDataGenerator(seed: Seed.F): fc.Arbitrary {
+ return fold>(
+ (x) => GeneratorByTag[bySeed[x[0]]](x as never)
+ )(seed as never)
+}
+
+/**
+ * ## {@link seedToInvalidDataGenerator `seedToInvalidDataGenerator`}
+ *
+ * Convert a seed into an invalid data generator.
+ *
+ * Invalid in this context means that it will never satisfy the zod schema that the seed produces.
+ *
+ * To convert a seed to a zod schema, use {@link seedToSchema `seedToSchema`}.
+ *
+ * To convert a seed to an _valid_ data generator, use
+ * {@link seedToValidDataGenerator `seedToValidDataGenerator`}.
+ */
+export function seedToInvalidDataGenerator(seed: T): fc.Arbitrary
+export function seedToInvalidDataGenerator(seed: Seed.F): fc.Arbitrary
+export function seedToInvalidDataGenerator(seed: Seed.F): fc.Arbitrary {
+ return fold>((x) => {
+ switch (x[0]) {
+ case byTag.record: return GeneratorByTag.record(x).map(mutateRandomValueOf)
+ case byTag.array: return GeneratorByTag.array(x).map(mutateRandomElementOf)
+ case byTag.tuple: return GeneratorByTag.tuple(x).map(mutateRandomElementOf)
+ case byTag.object: return GeneratorByTag.object(x).map(mutateRandomValueOf)
+ default: return GeneratorByTag[bySeed[x[0]]](x as never)
+ // default: return GeneratorByTag[bySeed[x[0]]](x as never, $)
+ }
+ })(seed as never)
+}
+
+/**
+ * ## {@link SeedGenerator `SeedGenerator`}
+ *
+ * Pseudo-random seed generator.
+ *
+ * The generator supports a wide range of discoverable configuration options via the
+ * optional `options` argument.
+ *
+ * Many of those options are forwarded to the corresponding `fast-check` arbitrary.
+ *
+ * **Note:** support for `options.minDepth` is experimental. If you use it, be advised that
+ * even with a minimum depth of 1, the schemas produced will be quite large. Using this option
+ * in your CI/CD pipeline is not recommended.
+ *
+ * See also:
+ * - {@link SeedGenerator `SeedGenerator`}
+ *
+ * @example
+ * // TODO
+ */
+export const SeedGenerator = Gen(SeedMap)
+
+export const seedsThatPreventGeneratingValidData = [
+ 'never',
+] satisfies SchemaGenerator.Options['exclude']
+
+export const seedsThatPreventGeneratingInvalidData = [
+ 'never',
+ 'unknown',
+] satisfies SchemaGenerator.Options['exclude']
+
+/**
+ * ## {@link SeedValidDataGenerator `SeedValidDataGenerator`}
+ *
+ * A seed generator that can be interpreted to produce reliably valid data.
+ *
+ * This was originally developed to test for parity between various schema libraries.
+ *
+ * Note that certain schemas make generating valid data impossible
+ * (like {@link JsonSchema.Never `JsonSchema.Never`}). For this reason, those schemas
+ * will not be seeded.
+ *
+ * To see the list of excluded schemas, see
+ * {@link seedsThatPreventGeneratingValidData `seedsThatPreventGeneratingValidData`}.
+ *
+ * See also:
+ * - {@link SeedInvalidDataGenerator `SeedInvalidDataGenerator`}
+ *
+ * @example
+ * // TODO
+ */
+
+export const SeedValidDataGenerator = SeedGenerator({ exclude: seedsThatPreventGeneratingValidData })['*']
+
+/**
+ * ## {@link SeedInvalidDataGenerator `zx.SeedInvalidDataGenerator`}
+ *
+ * A seed generator that can be interpreted to produce reliably invalid data.
+ *
+ * This was originally developed to test for parity between various schema libraries.
+ *
+ * Note that certain schemas make generating invalid data impossible
+ * (like {@link JsonSchema.Unknown `JsonSchema.Unknown`}). For this reason, those schemas
+ * will not be seeded.
+ *
+ * To see the list of excluded schemas, see
+ * {@link seedsThatPreventGeneratingInvalidData `zx.seedsThatPreventGeneratingInvalidData`}.
+ *
+ * See also:
+ * - {@link SeedValidDataGenerator `zx.SeedValidDataGenerator`}
+ *
+ * @example
+ * // TODO
+ */
+export const SeedInvalidDataGenerator = fn.pipe(
+ SeedGenerator({ exclude: seedsThatPreventGeneratingInvalidData }),
+ ($) => fc.oneof(
+ $.object,
+ $.tuple,
+ $.array,
+ $.record,
+ )
+)
+
+/**
+ * ## {@link SchemaGenerator `zx.SchemaGenerator`}
+ *
+ * A zod schema generator that can be interpreted to produce an arbitrary `zod` schema (v4, classic).
+ *
+ * The generator supports a wide range of configuration options that are discoverable via the
+ * optional `options` argument.
+ *
+ * Many of those options are forwarded to the corresponding `fast-check` arbitrary.
+ *
+ * **Note:** support for `options.minDepth` is experimental. If you use it, be advised that
+ * _even with a minimum depth of 1_, the schemas produced will be **quite** large. Using this option
+ * in your CI/CD pipeline is _not_ recommended.
+ *
+ * See also:
+ * - {@link SeedGenerator `zx.SeedGenerator`}
+ *
+ * @example
+ * // TODO
+ */
+
+export const SchemaGenerator = fn.flow(
+ SeedGenerator,
+ builder => builder['*'],
+ (arb) => arb.map(seedToSchema),
+)
+
+export declare namespace SchemaGenerator {
+ type Options = Config.Options
+}
+
+/**
+ * ## {@link seedToSchema `zx.seedToSchema`}
+ *
+ * Interpreter that converts a seed value into the corresponding zod schema.
+ *
+ * To get a seed, use {@link SeedGenerator `zx.SeedGenerator`}.
+ */
+// export function seedToSchema(seed: T): Seed.schemaFromComposite[T[0]]
+export function seedToSchema(seed: Seed.F): JsonSchema
+export function seedToSchema(seed: Seed.F) {
+ return fold((x) => {
+ switch (true) {
+ default: return fn.exhaustive(x)
+ case x[0] === byTag.boolean: return { type: 'boolean' }
+ case x[0] === byTag.never: return { enum: [] as [] }
+ case x[0] === byTag.null: return { type: 'null' }
+ case x[0] === byTag.unknown: return {}
+ case x[0] === byTag.integer: return JsonSchema_Integer(x[1])
+ case x[0] === byTag.number: return JsonSchema_Number(x[1])
+ case x[0] === byTag.string: return JsonSchema_String(x[1])
+ case x[0] === byTag.const: return { const: x[1] }
+ case x[0] === byTag.enum: return { enum: x[1] }
+ case x[0] === byTag.array: return JsonSchema_Array(x[1], x[2])
+ case x[0] === byTag.intersection: return { allOf: x[1] }
+ case x[0] === byTag.record: return {
+ type: 'object',
+ ...x[1] && { additionalProperties: x[1] },
+ ...x[2] && { patternProperties: { [x[2][0]]: x[2][1] } },
+ }
+ case x[0] === byTag.object: return { type: 'object', properties: Object_fromEntries(x[1]), required: x[2] ?? [] }
+ case x[0] === byTag.tuple: return { type: 'array', prefixItems: x[1] }
+ case x[0] === byTag.union: return { anyOf: x[1] }
+ }
+ })(seed as never)
+}
+
+/**
+ * ## {@link seedToValidData `seedToValidData`}
+ *
+ * Given a seed, generates an single example of valid data.
+ *
+ * Valid in this context means that it will always satisfy the zod schema that the seed produces.
+ *
+ * To convert a seed to a zod schema, use {@link seedToSchema `seedToSchema`}.
+ *
+ * To convert a seed to a single example of _invalid_ data, use {@link seedToInvalidData `seedToInvalidData`}.
+ */
+export const seedToValidData = fn.flow(
+ seedToValidDataGenerator,
+ (model) => fc.sample(model, 1)[0],
+)
+
+/**
+ * ## {@link seedToInvalidData `seedToInvalidData`}
+ *
+ * Given a seed, generates an single example of invalid data.
+ *
+ * Invalid in this context means that it will never satisfy the zod schema that the seed produces.
+ *
+ * To convert a seed to a zod schema, use {@link seedToSchema `seedToSchema`}.
+ *
+ * To convert a seed to a single example of _valid_ data, use {@link seedToValidData `seedToValidData`}.
+ */
+export const seedToInvalidData = fn.flow(
+ seedToInvalidDataGenerator,
+ (model) => fc.sample(model, 1)[0],
+)
diff --git a/packages/json-schema-test/src/index.ts b/packages/json-schema-test/src/index.ts
new file mode 100644
index 00000000..34c78cc6
--- /dev/null
+++ b/packages/json-schema-test/src/index.ts
@@ -0,0 +1,2 @@
+export * from './exports.js'
+export * as jsonSchemaTest from './exports.js'
diff --git a/packages/json-schema-test/src/version.ts b/packages/json-schema-test/src/version.ts
new file mode 100644
index 00000000..660ff1ca
--- /dev/null
+++ b/packages/json-schema-test/src/version.ts
@@ -0,0 +1,3 @@
+import pkg from './__generated__/__manifest__.js'
+export const VERSION = `${pkg.name}@${pkg.version}` as const
+export type VERSION = typeof VERSION
diff --git a/packages/json-schema-test/test/generator.fuzz.test.ts b/packages/json-schema-test/test/generator.fuzz.test.ts
new file mode 100644
index 00000000..c94a0bb7
--- /dev/null
+++ b/packages/json-schema-test/test/generator.fuzz.test.ts
@@ -0,0 +1,72 @@
+import * as vi from 'vitest'
+import * as fc from 'fast-check'
+
+import type { JsonSchema } from '@traversable/json-schema-types'
+import { check } from '@traversable/json-schema-types'
+import { jsonSchemaTest } from '@traversable/json-schema-test'
+
+const stringify = (_: string, v: unknown) =>
+ typeof v === 'symbol' ? String(v) : typeof v === 'bigint' ? `${v}n` : v
+
+type LogFailureDeps = {
+ msg: string
+ seed: jsonSchemaTest.Seed.Seed.Fixpoint
+ schema: JsonSchema
+ errors?: unknown[]
+ data: unknown
+}
+
+const fail = (e: unknown, { msg, seed, data }: LogFailureDeps) => {
+ console.group(`\r\n\nFAILURE: ${msg}`)
+ console.error('\r\nError:', e)
+ console.debug('\r\nseed: ', JSON.stringify(seed, stringify, 2))
+ console.debug('\r\ndata: ', data === '' ? '' : data)
+ console.debug('\r\n\n')
+ console.groupEnd()
+ vi.assert.fail(`\r\nFAILURE: ${msg}`)
+}
+
+vi.describe('ใ๏ธโณ๏ธใโนโนโน โฒ@traversable/json-schema-testโณ', () => {
+ vi.it('ใ๏ธโณ๏ธใโบ โฒjsonSchemaTest.SeedValidDataGeneratorโณ: integration test', () => {
+ fc.assert(
+ fc.property(
+ jsonSchemaTest.SeedValidDataGenerator,
+ (seed) => {
+ const schema = jsonSchemaTest.seedToSchema(seed)
+ const validData = jsonSchemaTest.seedToValidData(seed)
+ const result = check.classic(schema)(validData)
+
+ if (result === false) {
+ console.log('schema', schema)
+ fail('expected check to succeed', { msg: 'check(schema)(validData)', data: validData, schema, seed })
+ vi.assert.fail()
+ }
+ }
+ ), {
+ endOnFailure: true,
+ examples: [
+ [[200, [-0, null, null, true, false]]]
+ ],
+ // numRuns: 10_000,
+ })
+ })
+
+ vi.it('ใ๏ธโณ๏ธใโบ โฒjsonSchemaTest.SeedInvalidDataGeneratorโณ: integration test', () => {
+ fc.assert(
+ fc.property(
+ jsonSchemaTest.SeedInvalidDataGenerator,
+ (seed) => {
+ const schema = jsonSchemaTest.seedToSchema(seed)
+ const invalidData = jsonSchemaTest.seedToInvalidData(seed)
+ const result = check.classic(schema)(invalidData)
+ if (result === true) {
+ fail('expected check to fail', { msg: 'check(schema)(validData)', data: invalidData, schema, seed })
+ vi.assert.fail()
+ }
+ }
+ ), {
+ endOnFailure: true,
+ // numRuns: 10_000,
+ })
+ })
+})
diff --git a/packages/json-schema-test/test/version.test.ts b/packages/json-schema-test/test/version.test.ts
new file mode 100644
index 00000000..39372410
--- /dev/null
+++ b/packages/json-schema-test/test/version.test.ts
@@ -0,0 +1,10 @@
+import * as vi from 'vitest'
+import pkg from '../package.json' with { type: 'json' }
+import { VERSION } from '@traversable/json-schema-test'
+
+vi.describe('ใโณ๏ธใโนโนโน โฒ@traversable/json-schema-testโณ', () => {
+ vi.it('ใโณ๏ธใโบ โฒVERSIONโณ', () => {
+ const expected = `${pkg.name}@${pkg.version}`
+ vi.assert.equal(VERSION, expected)
+ })
+})
diff --git a/packages/json-schema-test/tsconfig.build.json b/packages/json-schema-test/tsconfig.build.json
new file mode 100644
index 00000000..3f5dbdaa
--- /dev/null
+++ b/packages/json-schema-test/tsconfig.build.json
@@ -0,0 +1,15 @@
+{
+ "extends": "./tsconfig.src.json",
+ "compilerOptions": {
+ "tsBuildInfoFile": ".tsbuildinfo/build.tsbuildinfo",
+ "types": ["node"],
+ "declarationDir": "build/dts",
+ "outDir": "build/esm",
+ "stripInternal": true
+ },
+ "references": [
+ { "path": "../json" },
+ { "path": "../json-schema-types" },
+ { "path": "../registry" }
+ ]
+}
diff --git a/packages/json-schema-test/tsconfig.json b/packages/json-schema-test/tsconfig.json
new file mode 100644
index 00000000..2c291d21
--- /dev/null
+++ b/packages/json-schema-test/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "include": [],
+ "references": [
+ { "path": "tsconfig.src.json" },
+ { "path": "tsconfig.test.json" }
+ ]
+}
diff --git a/packages/json-schema-test/tsconfig.src.json b/packages/json-schema-test/tsconfig.src.json
new file mode 100644
index 00000000..ac21055d
--- /dev/null
+++ b/packages/json-schema-test/tsconfig.src.json
@@ -0,0 +1,15 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "tsBuildInfoFile": ".tsbuildinfo/src.tsbuildinfo",
+ "rootDir": "src",
+ "types": ["node"],
+ "outDir": "build/src"
+ },
+ "references": [
+ { "path": "../json" },
+ { "path": "../json-schema-types" },
+ { "path": "../registry" }
+ ],
+ "include": ["src"]
+}
diff --git a/packages/json-schema-test/tsconfig.test.json b/packages/json-schema-test/tsconfig.test.json
new file mode 100644
index 00000000..03b368eb
--- /dev/null
+++ b/packages/json-schema-test/tsconfig.test.json
@@ -0,0 +1,16 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "tsBuildInfoFile": ".tsbuildinfo/test.tsbuildinfo",
+ "rootDir": "test",
+ "types": ["node"],
+ "noEmit": true
+ },
+ "references": [
+ { "path": "tsconfig.src.json" },
+ { "path": "../json" },
+ { "path": "../json-schema-types" },
+ { "path": "../registry" }
+ ],
+ "include": ["test"]
+}
diff --git a/packages/json-schema-test/vite.config.ts b/packages/json-schema-test/vite.config.ts
new file mode 100644
index 00000000..64dba4ad
--- /dev/null
+++ b/packages/json-schema-test/vite.config.ts
@@ -0,0 +1,6 @@
+import { defineConfig, mergeConfig } from 'vitest/config'
+import sharedConfig from '../../vite.config.js'
+
+const localConfig = defineConfig({})
+
+export default mergeConfig(sharedConfig, localConfig)
\ No newline at end of file
diff --git a/packages/json-schema-types/README.md b/packages/json-schema-types/README.md
new file mode 100644
index 00000000..cb1ee0fd
--- /dev/null
+++ b/packages/json-schema-types/README.md
@@ -0,0 +1,42 @@
+
+แฏ๐๐ฟ๐ฎ๐๐ฒ๐ฟ๐๐ฎ๐ฏ๐น๐ฒ/๐ท๐๐ผ๐ป-๐๐ฐ๐ต๐ฒ๐บ๐ฎ-๐๐๐ฝ๐ฒ๐
+
+
+
+ A package containing Json Schema types and type predicates.
+
+
+
+
+
+
+

+
+

+
+
+
+
+
+
+
+## Why?
+
+The types in `@traversable/json-schema-types` are non-recursive.
diff --git a/packages/json-schema-types/package.json b/packages/json-schema-types/package.json
new file mode 100644
index 00000000..fdb96d92
--- /dev/null
+++ b/packages/json-schema-types/package.json
@@ -0,0 +1,46 @@
+{
+ "name": "@traversable/json-schema-types",
+ "type": "module",
+ "version": "0.0.0",
+ "private": false,
+ "description": "",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/traversable/schema.git",
+ "directory": "packages/json-schema-types"
+ },
+ "bugs": {
+ "url": "https://github.com/traversable/schema/issues",
+ "email": "ahrjarrett@gmail.com"
+ },
+ "@traversable": {
+ "generateExports": { "include": ["**/*.ts"] },
+ "generateIndex": { "include": ["**/*.ts"] }
+ },
+ "publishConfig": {
+ "access": "public",
+ "directory": "dist",
+ "registry": "https://registry.npmjs.org"
+ },
+ "scripts": {
+ "bench": "echo NOTHING TO BENCH",
+ "build": "pnpm build:esm && pnpm build:cjs && pnpm build:annotate",
+ "build:annotate": "babel build --plugins annotate-pure-calls --out-dir build --source-maps",
+ "build:esm": "tsc -b tsconfig.build.json",
+ "build:cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps",
+ "check": "tsc -b tsconfig.json",
+ "clean": "pnpm run \"/^clean:.*/\"",
+ "clean:build": "rm -rf .tsbuildinfo dist build",
+ "clean:deps": "rm -rf node_modules",
+ "test": "vitest"
+ },
+ "peerDependencies": {
+ "@traversable/json": "workspace:^",
+ "@traversable/registry": "workspace:^"
+ },
+ "devDependencies": {
+ "@traversable/json": "workspace:^",
+ "@traversable/registry": "workspace:^"
+ }
+}
diff --git a/packages/json-schema-types/src/__generated__/__manifest__.ts b/packages/json-schema-types/src/__generated__/__manifest__.ts
new file mode 100644
index 00000000..8e5d2c63
--- /dev/null
+++ b/packages/json-schema-types/src/__generated__/__manifest__.ts
@@ -0,0 +1,46 @@
+export default {
+ "name": "@traversable/json-schema-types",
+ "type": "module",
+ "version": "0.0.0",
+ "private": false,
+ "description": "",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/traversable/schema.git",
+ "directory": "packages/json-schema-types"
+ },
+ "bugs": {
+ "url": "https://github.com/traversable/schema/issues",
+ "email": "ahrjarrett@gmail.com"
+ },
+ "@traversable": {
+ "generateExports": { "include": ["**/*.ts"] },
+ "generateIndex": { "include": ["**/*.ts"] }
+ },
+ "publishConfig": {
+ "access": "public",
+ "directory": "dist",
+ "registry": "https://registry.npmjs.org"
+ },
+ "scripts": {
+ "bench": "echo NOTHING TO BENCH",
+ "build": "pnpm build:esm && pnpm build:cjs && pnpm build:annotate",
+ "build:annotate": "babel build --plugins annotate-pure-calls --out-dir build --source-maps",
+ "build:esm": "tsc -b tsconfig.build.json",
+ "build:cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps",
+ "check": "tsc -b tsconfig.json",
+ "clean": "pnpm run \"/^clean:.*/\"",
+ "clean:build": "rm -rf .tsbuildinfo dist build",
+ "clean:deps": "rm -rf node_modules",
+ "test": "vitest"
+ },
+ "peerDependencies": {
+ "@traversable/json": "workspace:^",
+ "@traversable/registry": "workspace:^"
+ },
+ "devDependencies": {
+ "@traversable/json": "workspace:^",
+ "@traversable/registry": "workspace:^"
+ }
+} as const
\ No newline at end of file
diff --git a/packages/json-schema-types/src/check.ts b/packages/json-schema-types/src/check.ts
new file mode 100644
index 00000000..06d0fa33
--- /dev/null
+++ b/packages/json-schema-types/src/check.ts
@@ -0,0 +1,679 @@
+import * as T from '@traversable/registry'
+import {
+ accessor,
+ escape,
+ escapeRegExp,
+ fn,
+ isQuoted,
+ Number_isSafeInteger,
+ Number_isFinite,
+ Math_min,
+ Math_max,
+ Number_isNatural,
+ Object_entries,
+ Object_keys,
+ Object_hasOwn,
+ Object_values,
+ parseKey,
+ stringifyKey,
+} from '@traversable/registry'
+import { Json } from '@traversable/json'
+
+import * as F from './functor.js'
+import { toType } from './to-type.js'
+import * as JsonSchema from './types.js'
+type JsonSchema = import('./types.js').JsonSchema
+
+export const checkJson = Json.fold<(x: unknown) => boolean>((x) => {
+ switch (true) {
+ default: return (void (x satisfies never), () => false)
+ case Json.isScalar(x):
+ return (u) => u === x
+ case Json.isArray(x):
+ return (u) => Json.isArray(u)
+ && u.length === x.length
+ && x.every((predicate, i) => predicate(u[i]))
+ case Json.isObject(x): {
+ const predicates = Object_entries(x)
+ return (u) => {
+ if (!Json.isObject(u)) {
+ return false
+ } else {
+ const keys = Object_keys(u)
+ if (keys.length !== predicates.map(([k]) => k).length)
+ return false
+ else
+ return predicates.every(
+ ([k, predicate]) =>
+ Object_hasOwn(u, k)
+ && predicate(u[k])
+ )
+ }
+ }
+ }
+ }
+})
+
+const fold = F.fold<(x: unknown) => boolean>((x) => {
+ switch (true) {
+ default: return (void (x satisfies never), () => false)
+ case JsonSchema.isNever(x): return () => false
+ case JsonSchema.isConst(x): return checkJson(x.const as Json<(x: unknown) => boolean>)
+ case JsonSchema.isNull(x): return (u) => u === null
+ case JsonSchema.isBoolean(x): return (u) => u === false || u === true
+ case JsonSchema.isUnion(x): {
+ if (x.anyOf.length === 0) return () => false
+ else if (x.anyOf.length === 1) return x.anyOf[0]
+ else return (u) => x.anyOf.some((p) => p(u))
+ }
+ case JsonSchema.isIntersection(x): {
+ if (x.allOf.length === 0) return () => true
+ else if (x.allOf.length === 1) return x.allOf[0]
+ else return (u) => x.allOf.every((p) => p(u))
+ }
+ case JsonSchema.isEnum(x): return (u) =>
+ u !== undefined
+ && Json.isScalar(u)
+ && x.enum.includes(u)
+ case JsonSchema.isInteger(x): {
+ const { maximum, minimum, multipleOf } = x
+ if (Number_isSafeInteger(maximum) && Number_isSafeInteger(minimum) && Number_isSafeInteger(multipleOf))
+ return (u) => Number_isSafeInteger(u)
+ && minimum <= u
+ && u <= maximum
+ && u % multipleOf === 0
+ else if (Number_isSafeInteger(maximum) && Number_isSafeInteger(minimum))
+ return (u) => Number_isSafeInteger(u)
+ && minimum <= u
+ && u <= maximum
+ else if (Number_isSafeInteger(maximum) && Number_isSafeInteger(multipleOf))
+ return (u) => Number_isSafeInteger(u)
+ && u <= maximum
+ && u % multipleOf === 0
+ else if (Number_isSafeInteger(maximum))
+ return (u) => Number_isSafeInteger(u)
+ && u <= maximum
+ else if (Number_isSafeInteger(minimum) && Number_isSafeInteger(multipleOf))
+ return (u) => Number_isSafeInteger(u)
+ && minimum <= u
+ && u % multipleOf === 0
+ else if (Number_isSafeInteger(minimum))
+ return (u) => Number_isSafeInteger(u)
+ && minimum <= u
+ else if (Number_isSafeInteger(multipleOf))
+ return (u) => Number_isSafeInteger(u)
+ && u % multipleOf === 0
+ else
+ return (u) => Number_isSafeInteger(u)
+ }
+ case JsonSchema.isNumber(x): {
+ const { exclusiveMaximum: xMax, exclusiveMinimum: xMin, maximum: max, minimum: min, multipleOf } = x
+ const maximum = Number_isFinite(xMax) && Number_isFinite(max) ? Math_min(xMax, max) : Number_isFinite(xMax) ? xMax : max
+ const minimum = Number_isFinite(xMin) && Number_isFinite(min) ? Math_max(xMin, min) : Number_isFinite(xMin) ? xMin : min
+ const exclusiveMaximum = Number_isFinite(xMax) && xMax === maximum
+ const exclusiveMinimum = Number_isFinite(xMin) && xMin === minimum
+ if (Number_isFinite(maximum) && Number_isFinite(minimum) && Number_isFinite(multipleOf))
+ if (exclusiveMaximum && exclusiveMinimum)
+ return (u) =>
+ Number_isFinite(u)
+ && minimum < u
+ && u < maximum
+ && u % multipleOf === 0
+ else if (exclusiveMaximum)
+ return (u) =>
+ Number_isFinite(u)
+ && minimum <= u
+ && u < maximum
+ && u % multipleOf === 0
+ else if (exclusiveMinimum)
+ return (u) =>
+ Number_isFinite(u)
+ && minimum < u
+ && u <= maximum
+ && u % multipleOf === 0
+ else
+ return (u) =>
+ Number_isFinite(u)
+ && minimum <= u
+ && u <= maximum
+ && u % multipleOf === 0
+ else if (Number_isFinite(maximum) && Number_isFinite(minimum))
+ if (exclusiveMaximum && exclusiveMinimum)
+ return (u) =>
+ Number_isFinite(u)
+ && minimum < u
+ && u < maximum
+ else if (exclusiveMaximum)
+ return (u) =>
+ Number_isFinite(u)
+ && minimum <= u
+ && u < maximum
+ else if (exclusiveMinimum)
+ return (u) =>
+ Number_isFinite(u)
+ && minimum < u
+ && u <= maximum
+ else
+ return (u) =>
+ Number_isFinite(u)
+ && minimum <= u
+ && u <= maximum
+ else if (Number_isFinite(maximum) && Number_isFinite(multipleOf))
+ if (exclusiveMaximum)
+ return (u) =>
+ Number_isFinite(u)
+ && u < maximum
+ && u % multipleOf === 0
+ else
+ return (u) =>
+ Number_isFinite(u)
+ && u <= maximum
+ && u % multipleOf === 0
+ else if (Number_isFinite(maximum))
+ if (exclusiveMaximum)
+ return (u) =>
+ Number_isFinite(u)
+ && u < maximum
+ else
+ return (u) =>
+ Number_isFinite(u)
+ && u <= maximum
+ else if (Number_isFinite(minimum) && Number_isFinite(multipleOf))
+ if (exclusiveMinimum)
+ return (u) =>
+ Number_isFinite(u)
+ && minimum < u
+ && u % multipleOf === 0
+ else
+ return (u) =>
+ Number_isFinite(u)
+ && minimum <= u
+ && u % multipleOf === 0
+ else if (Number_isFinite(minimum))
+ if (exclusiveMinimum)
+ return (u) =>
+ Number_isFinite(u)
+ && minimum < u
+ else
+ return (u) =>
+ Number_isFinite(u)
+ && minimum <= u
+ else if (Number_isFinite(multipleOf))
+ return (u) =>
+ Number_isFinite(u)
+ && u % multipleOf === 0
+ else
+ return (u) =>
+ Number_isFinite(u)
+ }
+ case JsonSchema.isString(x): {
+ const { maxLength, minLength } = x
+ if (Number_isNatural(maxLength) && Number_isNatural(minLength))
+ return (u) =>
+ typeof u === 'string'
+ && minLength <= u.length
+ && u.length <= maxLength
+ else if (Number_isNatural(maxLength))
+ return (u) =>
+ typeof u === 'string'
+ && u.length <= maxLength
+ else if (Number_isNatural(minLength))
+ return (u) =>
+ typeof u === 'string'
+ && minLength <= u.length
+ else
+ return (u) =>
+ typeof u === 'string'
+ }
+ case JsonSchema.isArray(x): {
+ const { items: predicate, maxItems, minItems } = x
+ if (predicate)
+ if (Number_isNatural(maxItems) && Number_isNatural(minItems))
+ return (u) =>
+ Json.isArray(u)
+ && u.every(predicate)
+ && minItems <= u.length
+ && u.length <= maxItems
+ else if (Number_isNatural(maxItems))
+ return (u) =>
+ Json.isArray(u)
+ && u.every(predicate)
+ && u.length <= maxItems
+ else if (Number_isNatural(minItems))
+ return (u) =>
+ Json.isArray(u)
+ && u.every(predicate)
+ && minItems <= u.length
+ else return (u) =>
+ Json.isArray(u)
+ && u.every(predicate)
+ else
+ if (Number_isNatural(maxItems) && Number_isNatural(minItems))
+ return (u) =>
+ Json.isArray(u)
+ && minItems <= u.length
+ && u.length <= maxItems
+ else if (Number_isNatural(maxItems))
+ return (u) =>
+ Json.isArray(u)
+ && u.length <= maxItems
+ else if (Number_isNatural(minItems))
+ return (u) =>
+ Json.isArray(u)
+ && minItems <= u.length
+ else
+ return Json.isArray
+ }
+ case JsonSchema.isTuple(x): {
+ const { items: predicate, maxItems, minItems, prefixItems: predicates } = x
+ if (predicate)
+ if (Number_isNatural(maxItems) && Number_isNatural(minItems))
+ return (u) =>
+ Json.isArray(u)
+ && minItems <= u.length
+ && u.length <= maxItems
+ && predicates.every((p, i) => p(u[i]))
+ && u.slice(predicates.length).every(predicate)
+ else if (Number_isNatural(maxItems))
+ return (u) =>
+ Json.isArray(u)
+ && u.length <= maxItems
+ && predicates.every((p, i) => p(u[i]))
+ && u.slice(predicates.length).every(predicate)
+ else if (Number_isNatural(minItems))
+ return (u) =>
+ Json.isArray(u)
+ && minItems <= u.length
+ && predicates.every((p, i) => p(u[i]))
+ && u.slice(predicates.length).every(predicate)
+ else return (u) =>
+ Json.isArray(u)
+ && predicates.every((p, i) => p(u[i]))
+ && u.slice(predicates.length).every(predicate)
+ else
+ if (Number_isNatural(maxItems) && Number_isNatural(minItems))
+ return (u) =>
+ Json.isArray(u)
+ && minItems <= u.length
+ && u.length <= maxItems
+ && predicates.every((p, i) => p(u[i]))
+ else if (Number_isNatural(maxItems))
+ return (u) =>
+ Json.isArray(u)
+ && u.length <= maxItems
+ && predicates.every((p, i) => p(u[i]))
+ else if (Number_isNatural(minItems))
+ return (u) =>
+ Json.isArray(u)
+ && minItems <= u.length
+ && predicates.every((p, i) => p(u[i]))
+ else return (u) =>
+ Json.isArray(u)
+ && predicates.every((p, i) => p(u[i]))
+ }
+ case JsonSchema.isRecord(x): {
+ const { additionalProperties, patternProperties } = x
+ if (additionalProperties && patternProperties) {
+ const patterns = Object_entries(patternProperties)
+ return (u) => {
+ if (!Json.isObject(u))
+ return false
+ else {
+ const keys = Object_keys(u)
+ return keys.every(
+ (key) => {
+ const [, predicate] = patterns.find(([p]) => new RegExp(escapeRegExp(p)).test(key)) || []
+ return (
+ predicate
+ && predicate(u[key])
+ )
+ || additionalProperties(u[key])
+ }
+ )
+ }
+ }
+ } else if (additionalProperties) {
+ return (u) =>
+ Json.isObject(u)
+ && Object_values(u).every(additionalProperties)
+ } else if (patternProperties) {
+ const patterns = Object_entries(patternProperties)
+ return (u) => {
+ if (!Json.isObject(u)) {
+ return false
+ } else {
+ const keys = Object_keys(u)
+ return keys.every(
+ (key) => {
+ const [, predicate] = patterns.find(([p]) => new RegExp(escapeRegExp(p)).test(key)) || []
+ return predicate && predicate(u[key])
+ }
+ )
+ }
+ }
+ } else return Json.isObject
+ }
+ case JsonSchema.isObject(x): {
+ const { properties, required } = x
+ const predicates = Object_entries(properties)
+ return (u) =>
+ Json.isObject(u)
+ && predicates.every(
+ ([k, predicate]) =>
+ !required.includes(k)
+ ? (
+ !Object_hasOwn(u, k)
+ || predicate(u[k])
+ )
+ : (
+ Object_hasOwn(u, k)
+ && predicate(u[k])
+ )
+ )
+ }
+ case JsonSchema.isUnknown(x): return () => true
+ }
+})
+
+function literalValueToString(x: Json.Scalar) {
+ if (typeof x === 'string') {
+ const escaped = escape(x)
+ return `"${escaped}"`
+ } else {
+ return typeof x === 'bigint' ? `${x}n` : `${x}`
+ }
+}
+
+const JsonFunctor: T.Functor.Ix = {
+ map: Json.Functor.map,
+ mapWithIndex(f) {
+ return (xs, VAR) => {
+ switch (true) {
+ default: return xs satisfies never
+ case Json.isScalar(xs): return xs
+ case Json.isArray(xs): return fn.map(xs, (x, i) => f(x, `${VAR}${accessor(i, false)}`, xs))
+ case Json.isObject(xs): return fn.map(xs, (x, k) => f(x, `${VAR}${accessor(k, false)}`, xs))
+ }
+ }
+ },
+}
+
+const foldJson = fn.catamorphism(JsonFunctor, 'value')
+
+function compileJson(x: Json, varName: string) {
+ return foldJson((x, VAR) => {
+ switch (true) {
+ default: return x satisfies never
+ case x == null:
+ case x === true:
+ case x === false:
+ case typeof x === 'number': return `${VAR} === ${x}`
+ case typeof x === 'string': return `${VAR} === "${escape(x)}"`
+ case Json.isArray(x): return `Array.isArray(${VAR}) && ${VAR}.length === ${x.length}${x.length === 0 ? '' : ' && '}${x.join(' && ')}`
+ case Json.isObject(x): {
+ const values = Object_values(x)
+ return `!!${VAR} && typeof ${VAR} === 'object'${values.length === 0 ? '' : ' && '}${values.join(' && ')}`
+ }
+ }
+ })(x as never, varName)
+}
+
+const compile = F.compile((x, ix, input) => {
+ const VAR = ix.varName
+ switch (true) {
+ default: return x satisfies never
+ case JsonSchema.isNever(x): return 'false'
+ case JsonSchema.isNull(x): return `${VAR} === null`
+ case JsonSchema.isBoolean(x): return `typeof ${VAR} === "boolean"`
+ case JsonSchema.isUnion(x): {
+ if (x.anyOf.length === 0) return 'false'
+ else if (x.anyOf.length === 1) return x.anyOf[0]
+ else return x.anyOf.length === 0 ? 'false' : `(${x.anyOf.map((v) => `(${v})`).join(' || ')})`
+ }
+ case JsonSchema.isIntersection(x): {
+ if (x.allOf.length === 0) return 'false'
+ else if (x.allOf.length === 1) return x.allOf[0]
+ return x.allOf.length === 0 ? 'true' : `(${x.allOf.map((v) => `(${v})`).join(' && ')})`
+ }
+ case JsonSchema.isEnum(x): {
+ const members = x.enum.map((v) => `${VAR} === ${literalValueToString(v)}`)
+ const OPEN = x.enum.length > 1 ? '(' : ''
+ const CLOSE = x.enum.length > 1 ? ')' : ''
+ return OPEN + members.join(' || ') + CLOSE
+ }
+ case JsonSchema.isConst(x): {
+ // TODO: handle arrays and objects
+ return compileJson(x.const, VAR)
+ }
+ case JsonSchema.isInteger(x): {
+ const { minimum: min, maximum: max, multipleOf } = x
+ const CHECK = `Number.isSafeInteger(${VAR})`
+ const MIN_CHECK = !Number_isSafeInteger(min) ? '' : ` && ${min} <= ${VAR}`
+ const MAX_CHECK = !Number_isSafeInteger(max) ? '' : ` && ${VAR} <= ${max}`
+ const MULTIPLE_OF = !Number_isSafeInteger(multipleOf) ? '' : ` && ${VAR} % ${multipleOf} === 0`
+ const OPEN = MIN_CHECK.length > 0 || MAX_CHECK.length > 0 ? '(' : ''
+ const CLOSE = MIN_CHECK.length > 0 || MAX_CHECK.length > 0 ? ')' : ''
+ return ''
+ + OPEN
+ + CHECK
+ + MIN_CHECK
+ + MAX_CHECK
+ + MULTIPLE_OF
+ + CLOSE
+ }
+ case JsonSchema.isNumber(x): {
+ const { minimum: min, maximum: max, exclusiveMinimum: xMin, exclusiveMaximum: xMax, multipleOf } = x
+ const CHECK = `Number.isFinite(${VAR})`
+ const MIN_CHECK
+ = Number_isFinite(xMin) ? ` && ${xMin} < ${VAR}`
+ : Number_isFinite(min) ? ` && ${min} <= ${VAR}`
+ : ''
+ const MAX_CHECK
+ = Number_isFinite(xMax) ? ` && ${VAR} < ${xMax}`
+ : Number_isFinite(max) ? ` && ${VAR} <= ${max}`
+ : ''
+ const MULTIPLE_OF = !Number_isFinite(multipleOf) ? '' : ` && ${VAR} % ${multipleOf} === 0`
+ const OPEN = MIN_CHECK.length > 0 || MAX_CHECK.length > 0 ? '(' : ''
+ const CLOSE = MIN_CHECK.length > 0 || MAX_CHECK.length > 0 ? ')' : ''
+ return ''
+ + OPEN
+ + CHECK
+ + MIN_CHECK
+ + MAX_CHECK
+ + MULTIPLE_OF
+ + CLOSE
+ }
+ case JsonSchema.isString(x): {
+ const { minLength: min, maxLength: max } = x
+ const CHECK = `typeof ${VAR} === "string"`
+ const MIN_CHECK = Number_isNatural(min) ? ` && ${min} <= ${VAR}.length` : ''
+ const MAX_CHECK = Number_isNatural(max) ? ` && ${VAR}.length <= ${max}` : ''
+ const OPEN = MIN_CHECK.length > 0 || MAX_CHECK.length > 0 ? '(' : ''
+ const CLOSE = MIN_CHECK.length > 0 || MAX_CHECK.length > 0 ? ')' : ''
+ return ''
+ + OPEN
+ + CHECK
+ + MIN_CHECK
+ + MAX_CHECK
+ + CLOSE
+ }
+ case JsonSchema.isTuple(x): {
+ const { items: rest, prefixItems, minItems, maxItems } = x
+ const REST = typeof rest !== 'string' ? '' : ` && ${VAR}.slice(${prefixItems.length}).every((value) => ${rest})`
+ const BODY = `${prefixItems.length > 0 ? ' && (' : ''}${prefixItems.join(' && ')}${prefixItems.length > 0 ? ')' : ''}`
+ const MIN_CHECK = Number_isNatural(minItems) ? ` && ${minItems} <= ${VAR}.length` : ''
+ const MAX_CHECK = Number_isNatural(maxItems) ? ` && ${VAR}.length <= ${maxItems}` : ''
+ return ''
+ + `Array.isArray(${VAR})`
+ + MIN_CHECK
+ + MAX_CHECK
+ + BODY
+ + REST
+ }
+ case JsonSchema.isArray(x): {
+ const { items, minItems, maxItems } = x
+ const BODY = ` && ${VAR}.every((value) => ${items})`
+ const MIN_CHECK = Number_isNatural(minItems) ? ` && ${minItems} <= ${VAR}.length` : ''
+ const MAX_CHECK = Number_isNatural(maxItems) ? ` && ${VAR}.length <= ${maxItems}` : ''
+ return ''
+ + `Array.isArray(${VAR})`
+ + MIN_CHECK
+ + MAX_CHECK
+ + BODY
+ }
+ case JsonSchema.isRecord(x): {
+ const { additionalProperties, patternProperties } = x
+ const CHECK = `!!${VAR} && typeof ${VAR} === "object"`
+ if (patternProperties !== undefined) {
+ const patterns = Object_entries(patternProperties)
+ return [
+ `${CHECK} && Object.entries(${VAR}).every(([key, value]) => {`,
+ ...patterns.map(([pattern, predicate]) => `if (/${pattern.length === 0 ? '^$' : pattern}/.test(key)) return ${predicate}`),
+ additionalProperties === undefined ? null : `return ${additionalProperties}`,
+ `return true`,
+ `})`
+ ].filter((_) => _ !== null).join('\n')
+ } else if (additionalProperties !== undefined) {
+ return `${CHECK} && Object.entries(${VAR}).every(([key, value]) => ${additionalProperties})`
+ } else {
+ return CHECK
+ }
+ }
+ case JsonSchema.isObject(x): {
+ const { properties, required } = x
+ const CHECK = `!!${VAR} && typeof ${VAR} === "object"`
+ const CHILDREN = Object_entries(properties).map(([k, v]) => !required.includes(k)
+ ? `(!Object.hasOwn(${VAR}, ${JSON.stringify(parseKey(k))}) || ${v})`
+ : v
+ )
+ const BODY = CHILDREN.length === 0 ? '' : ` && ${CHILDREN.join(' && ')}`
+ return ''
+ + CHECK
+ + BODY
+ }
+ case JsonSchema.isUnknown(x): return 'true'
+ }
+})
+
+function buildFunctionBody(schema: JsonSchema): string {
+ let BODY = compile(schema)
+ if (BODY.startsWith('(') && BODY.endsWith(')')) BODY = BODY.slice(1, -1)
+ return BODY
+}
+
+export declare namespace check {
+ type Options = toType.Options & {
+ /**
+ * Configure the name of the generated check function
+ * @default "check"
+ */
+ functionName?: string
+ /**
+ * Whether the returned predicate should infer the return type from
+ * the JSON Schema input and act as a type-guard for that type.
+ *
+ * For this to work properly, the JSON Schema you pass will need to
+ * be a _TypeScript value_ that has been declared with the `as const`
+ * modifier, or passed as a literal value (rather than a JSON value).
+ *
+ * **Note:** Inferring a type guard can be expensive, so by default
+ * this is an opt-in feature.
+ *
+ * @default false
+ */
+ asTypeGuard?: true
+ }
+}
+
+check.classic = check_classic
+check.writeable = check_writeable
+
+/**
+ * ## {@link check `JsonSchema.check`}
+ *
+ * Given a JSON Schema spec, returns a predicate function that accepts any input
+ * and returns a boolean indicating whether the input satisfied the spec.
+ *
+ * Pros:
+ * - {@link check `JsonSchema.check`} has better performance than {@link check_writeable `JsonSchema.check.writeable`}
+ *
+ * Cons:
+ * - {@link check `JsonSchema.check`} does not work in environments that disallow the use of the native
+ * [`Function` constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/Function),
+ * which means you can't use it with Cloudflare workers
+ *
+ * See also:
+ * - {@link check_classic `JsonSchema.check.classic`}
+ * - {@link check_writeable `JsonSchema.check.writeable`}
+ *
+ * @example
+ * import { check } from '@traversable/json-schema-types'
+ *
+ * const myCheck = check({ type: 'boolean' })
+ *
+ * console.log(myCheck(false)) // => true
+ * console.log(myCheck('true')) // => false
+ *
+ * const myTypeGuard = check({ type: 'boolean' }, { asTypeGuard: true })
+ *
+ * declare const input: unknown
+ *
+ * if (myTypeGuard(input)) {
+ * input
+ * // ^? const input: boolean
+ * }
+ */
+export function check(schema: T): (x: unknown) => boolean
+export function check(schema: T, options: Pick): (x: unknown) => x is toType
+export function check(schema: T) {
+ return globalThis.Function(
+ 'value',
+ 'return ' + buildFunctionBody(schema)
+ )
+}
+
+/**
+ * ## {@link check_writeable `JsonSchema.check.writeable`}
+ *
+ * Given a JSON Schema spec, returns a validation function in **stringified form**.
+ *
+ * Pros:
+ * - {@link check_writeable `JsonSchema.check.writeable`} has the same performance as
+ * {@link check `JsonSchema.check`} _and_ works in any environment
+ *
+ * Cons:
+ * - You have to write the schemas to disc somehow, and you'll definitely want to
+ * set up a build step that keeps them in sync with the JSON Schema spec.
+ *
+ * See also:
+ * - {@link check `JsonSchema.check`}
+ * - {@link check_classic `JsonSchema.check.classic`}
+ */
+function check_writeable(schema: T, options?: check.Options): string {
+ const FUNCTION_NAME = options?.functionName ?? 'check'
+ return `
+ function ${FUNCTION_NAME} (value) {
+ return ${buildFunctionBody(schema)}
+ }
+ `.trim()
+}
+
+/**
+ * ## {@link check_classic `JsonSchema.check.classic`}
+ *
+ * Given a JSON Schema spec, returns a predicate function that accepts any input
+ * and returns a boolean indicating whether the input satisfied the spec.
+ *
+ * Pros:
+ * - {@link check_classic `JsonSchema.check.classic`} works in any environment, including Cloudflare workers
+ *
+ * Cons:
+ * - {@link check_classic `JsonSchema.check.classic`} has worse performance than {@link check `JsonSchema.check`}
+ *
+ * See also:
+ * - {@link check `JsonSchema.check`}
+ * - {@link check_writeable `JsonSchema.check.writeable`}
+ */
+function check_classic(schema: T): (x: unknown) => boolean {
+ return fold(schema as JsonSchema<(x: unknown) => boolean>)
+}
diff --git a/packages/json-schema-types/src/exports.ts b/packages/json-schema-types/src/exports.ts
new file mode 100644
index 00000000..1ad96ed2
--- /dev/null
+++ b/packages/json-schema-types/src/exports.ts
@@ -0,0 +1,10 @@
+export * from './version.js'
+
+export * from './types.js'
+
+export type { Algebra, CompilerIndex, Index } from './functor.js'
+export { CompilerFunctor, Functor, fold, defaultCompilerIndex, defaultIndex } from './functor.js'
+
+export { check, checkJson } from './check.js'
+export { toType } from './to-type.js'
+export { TypeName, TypeNames } from './typename.js'
diff --git a/packages/json-schema-types/src/functor.ts b/packages/json-schema-types/src/functor.ts
new file mode 100644
index 00000000..f5da4c9b
--- /dev/null
+++ b/packages/json-schema-types/src/functor.ts
@@ -0,0 +1,230 @@
+import type * as T from '@traversable/registry'
+import { fn, accessor, symbol } from '@traversable/registry'
+import * as JsonSchema from './types.js'
+
+export interface Index {
+ dataPath: (string | number)[]
+ schemaPath: (keyof any)[]
+ isOptional: boolean
+}
+
+export interface CompilerIndex extends Index {
+ varName: string
+ isProperty: boolean
+}
+
+export type Algebra = (x: JsonSchema.F, ix: Index, input: JsonSchema.Fixpoint) => T
+export type IndexedAlgebra = (x: JsonSchema.F, ix: Ix, input: JsonSchema.Fixpoint) => T
+
+export const defaultIndex = {
+ dataPath: [],
+ schemaPath: [],
+ isOptional: false,
+} satisfies Index
+
+export const defaultCompilerIndex = {
+ ...defaultIndex,
+ isProperty: false,
+ varName: 'value',
+} satisfies CompilerIndex
+
+
+export const Functor: T.Functor.Ix = {
+ map(f) {
+ return (x) => {
+ switch (true) {
+ default: return x satisfies never
+ case JsonSchema.isNullary(x): return x
+ case JsonSchema.isArray(x): return { ...x, items: f(x.items) }
+ case JsonSchema.isObject(x): return { ...x, properties: fn.map(x.properties, f) }
+ case JsonSchema.isUnion(x): return { anyOf: fn.map(x.anyOf, f) }
+ case JsonSchema.isIntersection(x): return { allOf: fn.map(x.allOf, f) }
+ case JsonSchema.isTuple(x): {
+ const { items, prefixItems, ...xs } = x
+ return { ...xs, ...items && { items: f(items) }, prefixItems: fn.map(x.prefixItems, f) }
+ }
+ case JsonSchema.isRecord(x): {
+ const { additionalProperties: a, patternProperties: p, ...xs } = x
+ return { ...xs, ...a && { additionalProperties: f(a) }, ...p && { patternProperties: fn.map(p, f) } }
+ }
+ case JsonSchema.isUnknown(x): return x
+ }
+ }
+ },
+ mapWithIndex(f) {
+ return (x, ix) => {
+ switch (true) {
+ default: return x satisfies never
+ case JsonSchema.isNullary(x): return x
+ case JsonSchema.isArray(x): return {
+ ...x,
+ items: f(x.items, { ...ix, schemaPath: [...ix.schemaPath, symbol.array] }, x)
+ }
+ case JsonSchema.isUnion(x): return {
+ anyOf: fn.map(
+ x.anyOf,
+ (v) => f(v, { ...ix, schemaPath: [...ix.schemaPath, symbol.union] }, x)
+ )
+ }
+ case JsonSchema.isIntersection(x): return {
+ allOf: fn.map(
+ x.allOf,
+ (v) => f(v, { ...ix, schemaPath: [...ix.schemaPath, symbol.intersect] }, x)
+ )
+ }
+ case JsonSchema.isObject(x): {
+ return {
+ ...x,
+ properties: fn.map(
+ x.properties,
+ (v, k) => f(v, {
+ isOptional: !x.required.includes(k),
+ dataPath: [...ix.dataPath, k],
+ schemaPath: [...ix.schemaPath, k],
+ }, x)
+ )
+ }
+ }
+ case JsonSchema.isTuple(x): {
+ const { items, prefixItems, ...xs } = x
+ return {
+ ...xs,
+ ...items && { items: f(items, { ...ix, schemaPath: [...ix.schemaPath, symbol.tuple] }, x) },
+ prefixItems: fn.map(
+ prefixItems,
+ (v, i) => f(v, { ...ix, dataPath: [...ix.dataPath, i], schemaPath: [...ix.schemaPath, i] }, x)
+ )
+ }
+ }
+ case JsonSchema.isRecord(x): {
+ const { additionalProperties: a, patternProperties: p, ...xs } = x
+ return {
+ ...xs,
+ ...a && { additionalProperties: f(a, { ...ix, schemaPath: [...ix.schemaPath, symbol.record] }, x) },
+ ...p && { patternProperties: fn.map(p, (v, k) => f(v, { ...ix, schemaPath: [...ix.schemaPath, symbol.record, k] }, x)) },
+ }
+ }
+ case JsonSchema.isUnknown(x): return x
+ }
+ }
+ },
+}
+
+export const fold = fn.catamorphism(Functor, defaultIndex)
+
+export const CompilerFunctor: T.Functor.Ix = {
+ map: Functor.map,
+ mapWithIndex(g) {
+ return (x, _ix) => {
+ const ix = { ..._ix, isProperty: false } satisfies CompilerIndex
+ switch (true) {
+ default: return x satisfies never
+ case JsonSchema.isNullary(x): return x
+ case JsonSchema.isArray(x): return {
+ ...x,
+ items: g(x.items, {
+ dataPath: ix.dataPath,
+ isOptional: ix.isOptional,
+ isProperty: ix.isProperty,
+ schemaPath: [...ix.schemaPath, symbol.array],
+ varName: 'value',
+ }, x)
+ }
+ case JsonSchema.isUnion(x): return {
+ anyOf: fn.map(
+ x.anyOf,
+ (v, i) => g(v, {
+ dataPath: ix.dataPath,
+ isOptional: ix.isOptional,
+ isProperty: ix.isProperty,
+ schemaPath: [...ix.schemaPath, symbol.union, i],
+ varName: ix.varName,
+ }, x)
+ )
+ }
+ case JsonSchema.isIntersection(x): return {
+ allOf: fn.map(
+ x.allOf,
+ (v, i) => g(v, {
+ dataPath: ix.dataPath,
+ isOptional: ix.isOptional,
+ isProperty: ix.isProperty,
+ schemaPath: [...ix.schemaPath, symbol.intersect, i],
+ varName: ix.varName,
+ }, x)
+ )
+ }
+ case JsonSchema.isObject(x): {
+ return {
+ ...x,
+ properties: fn.map(
+ x.properties,
+ (v, k) => g(v, {
+ dataPath: [...ix.dataPath, k],
+ isOptional: !x.required.includes(k),
+ isProperty: true,
+ schemaPath: [...ix.schemaPath, k],
+ varName: ix.varName + accessor(k, ix.isOptional),
+ }, x)
+ )
+ }
+ }
+ case JsonSchema.isTuple(x): {
+ const { items, prefixItems, ...xs } = x
+ return {
+ ...xs,
+ ...items &&
+ ({
+ items: g(items, {
+ dataPath: ix.dataPath,
+ isOptional: ix.isOptional,
+ isProperty: false,
+ schemaPath: [...ix.schemaPath, symbol.tuple],
+ varName: 'value',
+ }, x)
+ }),
+ prefixItems: fn.map(
+ prefixItems,
+ (v, i) => g(v, {
+ dataPath: [...ix.dataPath, i],
+ isOptional: ix.isOptional,
+ isProperty: ix.isProperty,
+ schemaPath: [...ix.schemaPath, i],
+ varName: ix.varName + accessor(i, ix.isOptional),
+ }, x)
+ )
+ }
+ }
+ case JsonSchema.isRecord(x): {
+ const { additionalProperties: a, patternProperties: p, ...xs } = x
+ return {
+ ...xs,
+ ...a &&
+ ({
+ additionalProperties: g(a, {
+ dataPath: ix.dataPath,
+ isOptional: ix.isOptional,
+ isProperty: false,
+ schemaPath: [...ix.schemaPath, symbol.record],
+ varName: 'value',
+ }, x)
+ }),
+ ...p &&
+ ({
+ patternProperties: fn.map(p, (v, k) => g(v, {
+ dataPath: ix.dataPath,
+ isOptional: ix.isOptional,
+ isProperty: false,
+ schemaPath: [...ix.schemaPath, symbol.record, k],
+ varName: 'value',
+ }, x))
+ }),
+ }
+ }
+ case JsonSchema.isUnknown(x): return x
+ }
+ }
+ }
+}
+
+export const compile = fn.catamorphism(CompilerFunctor, defaultCompilerIndex)
diff --git a/packages/json-schema-types/src/index.ts b/packages/json-schema-types/src/index.ts
new file mode 100644
index 00000000..89b339e9
--- /dev/null
+++ b/packages/json-schema-types/src/index.ts
@@ -0,0 +1,3 @@
+export * from './exports.js'
+export type JsonSchema = import('./types.js').JsonSchema
+export * as JsonSchema from './exports.js'
diff --git a/packages/json-schema-types/src/to-type.ts b/packages/json-schema-types/src/to-type.ts
new file mode 100644
index 00000000..6e48cf4a
--- /dev/null
+++ b/packages/json-schema-types/src/to-type.ts
@@ -0,0 +1,109 @@
+import type { Force } from '@traversable/registry'
+import { escape, Object_entries, Object_keys, Object_values, parseKey, stringifyKey } from '@traversable/registry'
+import { Json } from '@traversable/json'
+
+import { fold } from './functor.js'
+import * as JsonSchema from './types.js'
+type JsonSchema = import('./types.js').JsonSchema
+
+const jsonSchemaToType = fold((x) => {
+ switch (true) {
+ default: return x satisfies never
+ case JsonSchema.isNever(x): return 'never'
+ case JsonSchema.isNull(x): return 'null'
+ case JsonSchema.isBoolean(x): return 'boolean'
+ case JsonSchema.isInteger(x): return 'number'
+ case JsonSchema.isNumber(x): return 'number'
+ case JsonSchema.isString(x): return 'string'
+ case JsonSchema.isConst(x): return Json.toString(x.const)
+ case JsonSchema.isUnion(x): return x.anyOf.join(' | ')
+ case JsonSchema.isIntersection(x): return x.allOf.join(' & ')
+ case JsonSchema.isArray(x): return `Array<${x.items}>`
+ case JsonSchema.isEnum(x): return x.enum.map((v) => typeof v === 'string' ? `"${escape(v)}"` : `${v}`).join(' | ')
+ case JsonSchema.isTuple(x): return `[${x.prefixItems.join(', ')}${typeof x.items === 'string' ? `, ...${x.items}[]` : ''}]`
+ case JsonSchema.isObject(x): {
+ const xs = Object_entries(x.properties).map(([k, v]) => `${parseKey(k)}${x.required.includes(k) ? '' : '?'}: ${v}`)
+ return xs.length === 0 ? '{}' : `{ ${xs.join(', ')} }`
+ }
+ case JsonSchema.isRecord(x): {
+ if (!x.patternProperties) return `Record`
+ else {
+ const patternKeys = Object_keys(x.patternProperties).map((k) => `${stringifyKey(k)}`).join(' | ')
+ const patternValues = Object_values(x.patternProperties).join(' | ')
+ return x.additionalProperties
+ ? `Record & Record<${patternKeys}, ${patternValues}>`
+ : `Record<${patternKeys}, ${patternValues}>`
+ }
+ }
+ case JsonSchema.isUnknown(x): return 'unknown'
+ }
+})
+
+/**
+ * ## {@link toType `JsonSchema.toType`}
+ *
+ * Convert a [JSON Schema](https://json-schema.org/) document into its corresponding TypeScript type.
+ *
+ * @example
+ * import { JsonSchema } from '@traversable/json-schema'
+ *
+ * const MyJsonSchema = { type: 'boolean' }
+ *
+ * console.log(JsonSchema.toType(MyJsonSchema))
+ * // => "boolean"
+ *
+ * // If you'd like to give the generated type a name, use the `typeName` option:
+ *
+ * console.log(JsonSchema.toType(MyJsonSchema, { typeName: 'MyType' }))
+ * // => "type MyType = boolean"
+ */
+export function toType(schema: JsonSchema, options?: toType.Options): string {
+ const TYPE_NAME = typeof options?.typeName === 'string' ? `type ${options.typeName} = ` : ''
+ return `${TYPE_NAME}${jsonSchemaToType(schema)}`
+}
+
+export declare namespace toType {
+ type Options = {
+ /**
+ * ### {@link Options `JsonSchema.Options.typeName`}
+ *
+ * By default, {@link toType `JsonSchema.toType`} will generate an "inline" TypeScript type.
+ * Use this option to have {@link toType `JsonSchema.toType`} generate a type alias with the
+ * name you provide.
+ */
+ typeName?: string
+ }
+}
+
+type Intersect = S extends [infer H, ...infer T] ? Intersect> : Out
+
+export type toType
+ = [keyof S] extends [never] ? unknown
+ : S extends { anyOf: infer T extends readonly any[] }
+ ? T[number] extends infer R ? R extends R ? toType : never : never
+ : S extends { allOf: infer T } ? Intersect
+ : S extends { type: 'null' } ? null
+ : S extends { type: 'boolean' } ? boolean
+ : S extends { type: 'integer' } ? number
+ : S extends { type: 'number' } ? number
+ : S extends { type: 'string' } ? string
+ : S extends { const: any } ? S['const']
+ : S extends { enum: readonly any[] } ? S['enum'][number]
+ : S extends { type: 'array', items: false, prefixItems: infer T } ? { [I in keyof T]: toType }
+ : S extends { type: 'array', items: any, prefixItems: readonly any[] }
+ ? [...S['prefixItems'], ...S['items'][]] extends infer T
+ ? { [I in keyof T]: toType }
+ : never
+ : S extends { type: 'array', prefixItems: infer T } ? { [I in keyof T]: toType }
+ : S extends { type: 'array', items: infer T } ? toType[]
+ : S extends { type: 'object', additionalProperties: infer R, patternProperties: infer T } ?
+ & Record>
+ & Record>
+ : S extends { type: 'object', additionalProperties: infer R } ? Record>
+ : S extends { type: 'object', patternProperties: infer T } ? Record>
+ : S extends { type: 'object', properties: infer T, required: infer KS extends string[] } ?
+ Force<
+ & { [K in keyof T as K extends KS[number] ? K : never]-?: toType }
+ & { [K in keyof T as K extends KS[number] ? never : K]+?: toType }
+ >
+ : never
diff --git a/packages/json-schema-types/src/typename.ts b/packages/json-schema-types/src/typename.ts
new file mode 100644
index 00000000..20c080ed
--- /dev/null
+++ b/packages/json-schema-types/src/typename.ts
@@ -0,0 +1,22 @@
+import { Object_keys } from '@traversable/registry'
+
+export type TypeName = typeof TypeName[keyof typeof TypeName]
+export const TypeName = {
+ never: 'never',
+ unknown: 'unknown',
+ null: 'null',
+ boolean: 'boolean',
+ integer: 'integer',
+ number: 'number',
+ string: 'string',
+ enum: 'enum',
+ const: 'const',
+ array: 'array',
+ tuple: 'tuple',
+ object: 'object',
+ record: 'record',
+ union: 'union',
+ intersection: 'intersection',
+} as const
+
+export const TypeNames = Object_keys(TypeName)
diff --git a/packages/json-schema-types/src/types.ts b/packages/json-schema-types/src/types.ts
new file mode 100644
index 00000000..e4190a66
--- /dev/null
+++ b/packages/json-schema-types/src/types.ts
@@ -0,0 +1,360 @@
+import type { Json } from '@traversable/json'
+import type { HKT } from '@traversable/registry'
+import { Array_isArray, has } from '@traversable/registry'
+
+/**
+ * # {@link JsonSchema `JsonSchema`}
+ *
+ * If {@link T `T`} is not defined, {@link JsonSchema `JsonSchema`} returns a recursive
+ * Json Schema type.
+ *
+ * If {@link T `T`} **is** defined, {@link JsonSchema `JsonSchema`} returns a _non-recursive_
+ * Json Schema type, where the "holes" (the parts that would be recursive) are filled with {@link T `T`}.
+ * This is what allows {@link JsonSchema `JsonSchema`} to support recursion schemes.
+ *
+ * See also:
+ * - the [spec](https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00)
+ */
+export type JsonSchema = [T] extends [never]
+ ? Fixpoint
+ : F
+
+/** ## {@link Never `JsonSchema.Never`} */
+export type Never =
+ | { enum: [] }
+ | { not: Unknown }
+
+/** ## {@link Unknown `JsonSchema.Unknown`} */
+export interface Unknown {}
+
+/** ## {@link Null `JsonSchema.Null`} */
+export interface Null { type: 'null' }
+
+/** ## {@link Boolean `JsonSchema.Boolean`} */
+export interface Boolean { type: 'boolean' }
+
+/** ## {@link Integer `JsonSchema.Integer`} */
+export interface Integer extends Bounds.Numeric { type: 'integer' }
+
+/** ## {@link Number `JsonSchema.Number`} */
+export interface Number extends Bounds.Number { type: 'number' }
+
+/** ## {@link String `JsonSchema.String`} */
+export interface String extends Bounds.String { type: 'string' }
+
+/** ## {@link Enum `JsonSchema.Enum`} */
+export interface Enum {
+ /**
+ * ### {@link Enum `JsonSchema.Enum.enum`}
+ *
+ * See also:
+ * - the [spec](https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.6.1.2)
+ */
+ enum: Exclude[]
+}
+
+/** ## {@link Const `JsonSchema.Const`} */
+export interface Const {
+ /**
+ * ### {@link Const `JsonSchema.Const.const`}
+ *
+ * See also:
+ * - the [spec](https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.6.1.3)
+ */
+ const: Json
+}
+
+/** ## {@link Array `JsonSchema.Array`} */
+export interface Array extends Bounds.Items {
+ type: 'array'
+ /**
+ * ### {@link Array `JsonSchema.Array.items`}
+ *
+ * See also:
+ * - the [spec](https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-00#rfc.section.10.3.1.2)
+ */
+ items: T
+}
+
+/** ## {@link Tuple `JsonSchema.Tuple`} */
+export interface Tuple extends Bounds.Items {
+ type: 'array'
+ /**
+ * ### {@link Tuple `JsonSchema.Tuple.items`}
+ *
+ * See also:
+ * - the [spec](https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-00#rfc.section.10.3.1.2)
+ */
+ items?: false | T
+ /**
+ * ### {@link Tuple `JsonSchema.Tuple.prefixItems`}
+ *
+ * See also:
+ * the [spec](https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-00#rfc.section.10.3.1.1)
+ */
+ prefixItems: T[]
+}
+
+/** ## {@link Object `JsonSchema.Object`} */
+export interface Object {
+ type: 'object'
+ required: string[]
+ /**
+ * ### {@link Object `JsonSchema.Object.properties`}
+ *
+ * See also:
+ * - the [spec](https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-00#rfc.section.10.3.2.1)
+ */
+ properties: { [x: string]: T }
+}
+
+/** ## {@link Record `JsonSchema.Record`} */
+export interface Record {
+ type: 'object'
+ /**
+ * ### {@link Record `JsonSchema.Record.additionalProperties`}
+ *
+ * See also:
+ * - the [spec](https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-00#rfc.section.10.3.2.3)
+ */
+ additionalProperties?: T
+ /**
+ * ### {@link Record `JsonSchema.Record.patternProperties`}
+ *
+ * See also:
+ * - the [spec](https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-00#rfc.section.10.3.2.2)
+ */
+ patternProperties?: globalThis.Record
+}
+
+/** ## {@link Union `JsonSchema.Union`} */
+export interface Union {
+ /**
+ * ### {@link Union `JsonSchema.Union.anyOf`}
+ *
+ * See also:
+ * - the [spec](https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-00#rfc.section.10.2.1.3)
+ */
+ anyOf: readonly T[]
+}
+
+/** ## {@link Intersection `JsonSchema.Intersection`} */
+export interface Intersection {
+ /**
+ * ### {@link Intersection `JsonSchema.Intersection.allOf`}
+ *
+ * See also:
+ * - the [spec](https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-00#rfc.section.10.2.1.1)
+ */
+ allOf: readonly T[]
+}
+
+export type Scalar =
+ | Null
+ | Boolean
+ | Integer
+ | Number
+ | String
+
+export type Nullary =
+ | Never
+ // | Unknown
+ | Scalar
+ | Enum
+ | Const
+
+export type Unary =
+ | Array
+ | Tuple
+ | Object
+ | Record
+ | Union
+ | Intersection
+
+export type Fixpoint =
+ | Nullary
+ | Unknown
+ | Array
+ | Tuple
+ | Object
+ | Record
+ | Union
+ | Intersection
+
+export type F =
+ | Nullary
+ | Unknown
+ | Array
+ | Tuple
+ | Object
+ | Record
+ | Union
+ | Intersection
+
+// | Unary
+
+export interface Free extends HKT { [-1]: F }
+
+export declare namespace Bounds {
+ interface Numeric {
+ /**
+ * See also:
+ * - the [spec](https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.6.2.4)
+ */
+ minimum?: number
+ /**
+ * See also:
+ * - the [spec](https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.6.2.2)
+ */
+ maximum?: number
+ /**
+ * See also:
+ * - the [spec](https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.6.2.1)
+ */
+ multipleOf?: number
+ }
+
+ interface Number extends Bounds.Numeric {
+ /**
+ * See also:
+ * - the [spec](https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.6.2.5)
+ */
+ exclusiveMinimum?: number
+ /**
+ * See also:
+ * - the [spec](https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.6.2.3)
+ */
+ exclusiveMaximum?: number
+ }
+
+ interface String {
+ /**
+ * See also:
+ * - the [spec](https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.6.3.2)
+ */
+ minLength?: number
+ /**
+ * See also:
+ * - the [spec](https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.6.3.1)
+ */
+ maxLength?: number
+ /**
+ * See also:
+ * - the [spec](https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.6.3.3)
+ */
+ pattern?: string
+ }
+
+ interface Items {
+ /**
+ * See also:
+ * - the [spec](https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.6.4.2)
+ */
+ minItems?: number
+ /**
+ * See also:
+ * - the [spec](https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.6.4.1)
+ */
+ maxItems?: number
+ }
+}
+
+export function isNever(x: unknown): x is Never {
+ return has('enum', (_): _ is [] => Array_isArray(_) && _.length === 0)(x)
+ || has('not', isUnknown)(x)
+}
+
+export function isConst(x: unknown): x is Const {
+ return has('const')(x)
+}
+
+export function isEnum(x: unknown): x is Enum {
+ return has('enum', (_): _ is Scalar[] => Array_isArray(_) && _.length > 0)(x)
+}
+
+export function isUnknown(x: unknown): x is Unknown {
+ return !!x && x !== null && Object.keys(x).length === 0
+}
+
+export function isNull(x: unknown): x is Null {
+ return has('type', (_) => _ === 'null')(x)
+}
+
+export function isInteger(x: unknown): x is Integer {
+ return has('type', (_) => _ === 'integer')(x)
+}
+
+export function isBoolean(x: unknown): x is Boolean {
+ return has('type', (_) => _ === 'boolean')(x)
+}
+
+export function isNumber(x: unknown): x is Number {
+ return has('type', (_) => _ === 'number')(x)
+}
+
+export function isString(x: unknown): x is String {
+ return has('type', (_) => _ === 'string')(x)
+}
+
+export function isArray(x: F): x is Array
+export function isArray(x: unknown): x is Array
+export function isArray(x: unknown): x is Array {
+ return has('type', (_) => _ === 'array')(x)
+ && !has('prefixItems')(x)
+}
+
+export function isTuple(x: F): x is Tuple
+export function isTuple(x: unknown): x is Tuple
+export function isTuple(x: unknown): x is Tuple {
+ return has('type', (_) => _ === 'array')(x)
+ && has('prefixItems')(x)
+}
+
+export function isObject(x: F): x is Object
+export function isObject(x: unknown): x is Object
+export function isObject(x: unknown): x is Object {
+ return has('type', (_) => _ === 'object')(x)
+ && !has('additionalProperties')(x)
+ && !has('patternProperties')(x)
+}
+
+export function isRecord(x: F): x is Record
+export function isRecord(x: unknown): x is Record
+export function isRecord(x: unknown): x is Record {
+ return has('type', (_) => _ === 'object')(x)
+ && (has('additionalProperties')(x) || has('patternProperties')(x))
+}
+
+export function isUnion(x: F): x is Union