From 2c2bdf16c18c0c646be2157a70c38d46e082f0ae Mon Sep 17 00:00:00 2001 From: David Blass Date: Wed, 5 Jun 2024 01:23:07 -0400 Subject: [PATCH 01/61] improve wording of type example --- ark/docs/src/content/docs/intro/your-first-type.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ark/docs/src/content/docs/intro/your-first-type.mdx b/ark/docs/src/content/docs/intro/your-first-type.mdx index 4e8f3e86d4..ee923476d2 100644 --- a/ark/docs/src/content/docs/intro/your-first-type.mdx +++ b/ark/docs/src/content/docs/intro/your-first-type.mdx @@ -56,7 +56,7 @@ const user = type({ }) ``` -If we want to decouple `device` from `user`, we just move it to its own type and reference it. +To decouple `device` from `user`, just move it to its own type and reference it. ```ts import { type } from "arktype" From 01583b0bd7e99f88916d3ec404cf99b31f306c40 Mon Sep 17 00:00:00 2001 From: David Blass Date: Thu, 6 Jun 2024 12:24:32 -0400 Subject: [PATCH 02/61] runtime error messages increase font size --- ark/docs/src/styles.css | 1 + 1 file changed, 1 insertion(+) diff --git a/ark/docs/src/styles.css b/ark/docs/src/styles.css index 2dd3e69ae5..d260b2745a 100644 --- a/ark/docs/src/styles.css +++ b/ark/docs/src/styles.css @@ -112,6 +112,7 @@ starlight-theme-select, /** used to display runtime errors on hover */ .twoslash .twoslash-popup-docs { color: #f85858; + font-size: small; } /* Firefox specific rules */ From 9e3e77224694c0b42b8d0dd2d97fc264551ff604 Mon Sep 17 00:00:00 2001 From: David Blass Date: Thu, 6 Jun 2024 14:02:42 -0400 Subject: [PATCH 03/61] rename RegexNode=>PatternNode --- ark/dark/arktype.scratch.ts | 2 +- .../content/docs/intro/your-first-type.mdx | 6 +++- ark/schema/__tests__/morphs.test.ts | 4 +-- ark/schema/__tests__/parse.bench.ts | 2 +- ark/schema/__tests__/parse.test.ts | 4 +-- ark/schema/__tests__/props.test.ts | 6 ++-- ark/schema/ast.ts | 8 +++--- ark/schema/inference.ts | 6 ++-- ark/schema/keywords/internal.ts | 2 +- ark/schema/keywords/utils/regex.ts | 6 ++-- ark/schema/keywords/validation.ts | 2 +- ark/schema/kinds.ts | 14 +++++----- ark/schema/refinements/regex.ts | 28 +++++++++---------- ark/schema/roots/intersection.ts | 4 +-- ark/schema/roots/root.ts | 6 ++-- ark/schema/shared/implement.ts | 4 +-- ark/type/__tests__/enclosed.test.ts | 2 +- ark/type/__tests__/pipe.test.ts | 2 +- ark/type/__tests__/regex.test.ts | 2 +- ark/type/__tests__/traverse.test.ts | 2 +- ark/type/parser/definition.ts | 2 +- .../parser/string/shift/operand/enclosed.ts | 2 +- ark/type/type.ts | 8 +++--- 23 files changed, 64 insertions(+), 60 deletions(-) diff --git a/ark/dark/arktype.scratch.ts b/ark/dark/arktype.scratch.ts index 65616737c4..1083fc446f 100644 --- a/ark/dark/arktype.scratch.ts +++ b/ark/dark/arktype.scratch.ts @@ -130,5 +130,5 @@ class F { const highlighted = type({ literals: "'foo' | 'bar' | true", expressions: "boolean[] | 5 < number <= 10 | number % 2", - regex: "/^(?:4[0-9]{12}(?:[0-9]{3,6}))$/" + pattern: "/^(?:4[0-9]{12}(?:[0-9]{3,6}))$/" }) diff --git a/ark/docs/src/content/docs/intro/your-first-type.mdx b/ark/docs/src/content/docs/intro/your-first-type.mdx index ee923476d2..fbb086c10b 100644 --- a/ark/docs/src/content/docs/intro/your-first-type.mdx +++ b/ark/docs/src/content/docs/intro/your-first-type.mdx @@ -1,5 +1,5 @@ --- -title: Your first type +title: Your First Type sidebar: order: 2 --- @@ -116,3 +116,7 @@ if (out instanceof type.errors) { console.log(`Hello, ${out.name}`) } ``` + +And that's it! You now know how to to define a `Type` use it to check your data at runtime. + +Next, we'll take a look at how ArkType extends TypeScript's type system to handle common runtime constraints like `minLength` and `pattern`. diff --git a/ark/schema/__tests__/morphs.test.ts b/ark/schema/__tests__/morphs.test.ts index 8c81558a67..a06e49b2b2 100644 --- a/ark/schema/__tests__/morphs.test.ts +++ b/ark/schema/__tests__/morphs.test.ts @@ -7,14 +7,14 @@ contextualize(() => { const parseNumber = schema({ in: { domain: "string", - regex: wellFormedNumberMatcher, + pattern: wellFormedNumberMatcher, description: "a well-formed numeric string" }, morphs: (s: string) => Number.parseFloat(s) }) attest(parseNumber.in.json).snap({ domain: "string", - regex: ["^(?!^-0$)-?(?:0|[1-9]\\d*)(?:\\.\\d*[1-9])?$"], + pattern: ["^(?!^-0$)-?(?:0|[1-9]\\d*)(?:\\.\\d*[1-9])?$"], description: "a well-formed numeric string" }) attest(parseNumber.out.json).snap({}) diff --git a/ark/schema/__tests__/parse.bench.ts b/ark/schema/__tests__/parse.bench.ts index 593f138a42..94df4ecfcb 100644 --- a/ark/schema/__tests__/parse.bench.ts +++ b/ark/schema/__tests__/parse.bench.ts @@ -9,5 +9,5 @@ bench("intersection", () => schema("string").and(schema("number"))).types([ ]) bench("no assignment", () => { - schema({ domain: "string", regex: "/.*/" }) + schema({ domain: "string", pattern: "/.*/" }) }).types([350, "instantiations"]) diff --git a/ark/schema/__tests__/parse.test.ts b/ark/schema/__tests__/parse.test.ts index b19cd5979d..4d843230b2 100644 --- a/ark/schema/__tests__/parse.test.ts +++ b/ark/schema/__tests__/parse.test.ts @@ -3,9 +3,9 @@ import { type Root, schema } from "@arktype/schema" contextualize(() => { it("single constraint", () => { - const t = schema({ domain: "string", regex: ".*" }) + const t = schema({ domain: "string", pattern: ".*" }) attest>(t) - attest(t.json).snap({ domain: "string", regex: [".*"] }) + attest(t.json).snap({ domain: "string", pattern: [".*"] }) }) it("multiple constraints", () => { diff --git a/ark/schema/__tests__/props.test.ts b/ark/schema/__tests__/props.test.ts index aded45c2b1..4da41cf139 100644 --- a/ark/schema/__tests__/props.test.ts +++ b/ark/schema/__tests__/props.test.ts @@ -63,13 +63,13 @@ contextualize(() => { }) }) - const startingWithA = schema({ domain: "string", regex: /^a.*/ }) + const startingWithA = schema({ domain: "string", pattern: /^a.*/ }) - const endingWithZ = schema({ domain: "string", regex: /.*z$/ }) + const endingWithZ = schema({ domain: "string", pattern: /.*z$/ }) const startingWithAAndEndingWithZ = schema({ domain: "string", - regex: [/^a.*/, /.*z$/] + pattern: [/^a.*/, /.*z$/] }) it("intersects nonsubtype index signatures", () => { diff --git a/ark/schema/ast.ts b/ark/schema/ast.ts index 4e1ef78fc7..2ae5f13b4e 100644 --- a/ark/schema/ast.ts +++ b/ark/schema/ast.ts @@ -16,7 +16,7 @@ export type Constraints = { divisor?: { [k: number]: 1 } min?: { [k: number | string]: 0 | 1 } max?: { [k: number | string]: 0 | 1 } - regex?: { [k: string]: 1 } + pattern?: { [k: string]: 1 } length?: { [k: number]: 1 } predicate?: 1 literal?: string | number @@ -66,7 +66,7 @@ export type Length = { } export type Matching = { - regex: { [k in rule]: 1 } + pattern: { [k in rule]: 1 } } export type Narrowed = { @@ -174,7 +174,7 @@ export namespace string { schema extends { exclusive: true } ? lessThanLength : atMostLength - : kind extends "regex" ? matching + : kind extends "pattern" ? matching : kind extends "exactLength" ? exactlyLength : narrowed : never @@ -273,7 +273,7 @@ export type schemaToConstraint< schema extends NodeSchema > = normalizePrimitiveConstraintRoot extends infer rule ? - kind extends "regex" ? Matching + kind extends "pattern" ? Matching : kind extends "divisor" ? DivisibleBy : kind extends "exactLength" ? Length : kind extends "min" ? diff --git a/ark/schema/inference.ts b/ark/schema/inference.ts index f0e9eed1d5..b0e590c763 100644 --- a/ark/schema/inference.ts +++ b/ark/schema/inference.ts @@ -148,12 +148,12 @@ export type inferBasis, $> = // ] // ? inferIndexed< // tail, -// entry["key"] extends { readonly regex: VariadicIndexMatcherLiteral } +// entry["key"] extends { readonly pattern: VariadicIndexMatcherLiteral } // ? result extends List // ? [...result, ...inferTypeInput[]] // : never // : entry["key"] extends { -// readonly regex: NonVariadicIndexMatcherLiteral +// readonly pattern: NonVariadicIndexMatcherLiteral // } // ? inferTypeInput[] // : Record< @@ -168,7 +168,7 @@ export type inferBasis, $> = // indexed extends readonly IndexedPropInput[] // > = [named, indexed[0]["key"]] extends // | [TupleLengthProps, unknown] -// | [unknown, { readonly regex: VariadicIndexMatcherLiteral }] +// | [unknown, { readonly pattern: VariadicIndexMatcherLiteral }] // ? inferNonVariadicTupleProps & // inferObjectLiteralProps> // : inferObjectLiteralProps diff --git a/ark/schema/keywords/internal.ts b/ark/schema/keywords/internal.ts index 427039f25d..fdffab786c 100644 --- a/ark/schema/keywords/internal.ts +++ b/ark/schema/keywords/internal.ts @@ -17,7 +17,7 @@ export const internalKeywords: internalKeywords = schemaScope( { lengthBoundable: ["string", Array], propertyKey: ["string", "symbol"], - nonNegativeIntegerString: { domain: "string", regex: arrayIndexMatcher } + nonNegativeIntegerString: { domain: "string", pattern: arrayIndexMatcher } }, { prereducedAliases: true, diff --git a/ark/schema/keywords/utils/regex.ts b/ark/schema/keywords/utils/regex.ts index 6e06d2991d..be8cf56d57 100644 --- a/ark/schema/keywords/utils/regex.ts +++ b/ark/schema/keywords/utils/regex.ts @@ -1,13 +1,13 @@ -import type { NormalizedRegexSchema } from "../../refinements/regex.js" +import type { NormalizedPatternSchema } from "../../refinements/regex.js" import { root } from "../../scope.js" export const defineRegex = ( regex: RegExp, description: string -): { domain: "string"; regex: NormalizedRegexSchema } => +): { domain: "string"; pattern: NormalizedPatternSchema } => root.defineRoot({ domain: "string", - regex: { + pattern: { rule: regex.source, flags: regex.flags, description diff --git a/ark/schema/keywords/validation.ts b/ark/schema/keywords/validation.ts index 10d339f525..5e5655da6d 100644 --- a/ark/schema/keywords/validation.ts +++ b/ark/schema/keywords/validation.ts @@ -43,7 +43,7 @@ const semver = defineRegex( const creditCard = root.defineRoot({ domain: "string", - regex: { + pattern: { rule: creditCardMatcher.source, description: "a valid credit card number" }, diff --git a/ark/schema/kinds.ts b/ark/schema/kinds.ts index d03172c6a3..e98542693f 100644 --- a/ark/schema/kinds.ts +++ b/ark/schema/kinds.ts @@ -17,9 +17,9 @@ import { type BoundNodesByKind } from "./refinements/kinds.js" import { - RegexNode, - regexImplementation, - type RegexDeclaration + PatternNode, + patternImplementation, + type PatternDeclaration } from "./refinements/regex.js" import { AliasNode, @@ -103,7 +103,7 @@ export interface NodeDeclarationsByKind extends BoundDeclarations { required: RequiredDeclaration optional: OptionalDeclaration index: IndexDeclaration - regex: RegexDeclaration + pattern: PatternDeclaration predicate: PredicateDeclaration structure: StructureDeclaration } @@ -121,7 +121,7 @@ export const nodeImplementationsByKind: Record< morph: morphImplementation, intersection: intersectionImplementation, divisor: divisorImplementation, - regex: regexImplementation, + pattern: patternImplementation, predicate: predicateImplementation, required: requiredImplementation, optional: optionalImplementation, @@ -143,7 +143,7 @@ export const nodeClassesByKind: Record< morph: MorphNode, intersection: IntersectionNode, divisor: DivisorNode, - regex: RegexNode, + pattern: PatternNode, predicate: PredicateNode, required: RequiredNode, optional: OptionalNode, @@ -161,7 +161,7 @@ interface NodesByKind extends BoundNodesByKind { proto: ProtoNode domain: DomainNode divisor: DivisorNode - regex: RegexNode + pattern: PatternNode predicate: PredicateNode required: RequiredNode optional: OptionalNode diff --git a/ark/schema/refinements/regex.ts b/ark/schema/refinements/regex.ts index 7d8ff8a031..7b7507c515 100644 --- a/ark/schema/refinements/regex.ts +++ b/ark/schema/refinements/regex.ts @@ -6,29 +6,29 @@ import { type nodeImplementationOf } from "../shared/implement.js" -export interface RegexInner extends BaseMeta { +export interface PatternInner extends BaseMeta { readonly rule: string readonly flags?: string } -export type NormalizedRegexSchema = RegexInner +export type NormalizedPatternSchema = PatternInner -export type RegexSchema = NormalizedRegexSchema | string | RegExp +export type PatternSchema = NormalizedPatternSchema | string | RegExp -export interface RegexDeclaration +export interface PatternDeclaration extends declareNode<{ - kind: "regex" - schema: RegexSchema - normalizedSchema: NormalizedRegexSchema - inner: RegexInner + kind: "pattern" + schema: PatternSchema + normalizedSchema: NormalizedPatternSchema + inner: PatternInner intersectionIsOpen: true prerequisite: string - errorContext: RegexInner + errorContext: PatternInner }> {} -export const regexImplementation: nodeImplementationOf = - implementNode({ - kind: "regex", +export const patternImplementation: nodeImplementationOf = + implementNode({ + kind: "pattern", collapsibleKey: "rule", keys: { rule: {}, @@ -49,11 +49,11 @@ export const regexImplementation: nodeImplementationOf = intersections: { // for now, non-equal regex are naively intersected: // https://github.com/arktypeio/arktype/issues/853 - regex: () => null + pattern: () => null } }) -export class RegexNode extends RawPrimitiveConstraint { +export class PatternNode extends RawPrimitiveConstraint { readonly instance: RegExp = new RegExp(this.rule, this.flags) readonly expression: string = `${this.instance}` traverseAllows: (string: string) => boolean = this.instance.test.bind( diff --git a/ark/schema/roots/intersection.ts b/ark/schema/roots/intersection.ts index 0f6f4430db..0f56f9bc8c 100644 --- a/ark/schema/roots/intersection.ts +++ b/ark/schema/roots/intersection.ts @@ -302,9 +302,9 @@ export const intersectionImplementation: nodeImplementationOf -export type BoundKind = Exclude +export type BoundKind = Exclude export const refinementKinds = [ - "regex", + "pattern", "divisor", "exactLength", "max", diff --git a/ark/type/__tests__/enclosed.test.ts b/ark/type/__tests__/enclosed.test.ts index 5aa83149ff..cdb2f72dad 100644 --- a/ark/type/__tests__/enclosed.test.ts +++ b/ark/type/__tests__/enclosed.test.ts @@ -13,7 +13,7 @@ contextualize(() => { const t = type("'foo'|/.*/[]") attest<"foo" | string[]>(t.infer) attest(t.json).snap([ - { proto: "Array", sequence: { domain: "string", regex: [".*"] } }, + { proto: "Array", sequence: { domain: "string", pattern: [".*"] } }, { unit: "foo" } ]) }) diff --git a/ark/type/__tests__/pipe.test.ts b/ark/type/__tests__/pipe.test.ts index 11dd984689..d60ddd07c2 100644 --- a/ark/type/__tests__/pipe.test.ts +++ b/ark/type/__tests__/pipe.test.ts @@ -28,7 +28,7 @@ contextualize(() => { attest(t.json).snap({ in: { domain: "string", - regex: [ + pattern: [ { description: "a well-formed numeric string", flags: "", diff --git a/ark/type/__tests__/regex.test.ts b/ark/type/__tests__/regex.test.ts index 3b3e2d5526..b64ce1be61 100644 --- a/ark/type/__tests__/regex.test.ts +++ b/ark/type/__tests__/regex.test.ts @@ -79,7 +79,7 @@ contextualize( // @ts-expect-error attest(() => type("number").matching("foo")).throwsAndHasTypeError( writeInvalidOperandMessage( - "regex", + "pattern", keywordNodes.string, keywordNodes.number ) diff --git a/ark/type/__tests__/traverse.test.ts b/ark/type/__tests__/traverse.test.ts index f03eced4bc..d1c1e64b2f 100644 --- a/ark/type/__tests__/traverse.test.ts +++ b/ark/type/__tests__/traverse.test.ts @@ -20,7 +20,7 @@ contextualize(() => { attest(t("foo").toString()).snap("must be a number (was string)") }) - it("regex", () => { + it("pattern", () => { const t = type("/.*@arktype.io/") attest(t("shawn@arktype.io")).snap("shawn@arktype.io") attest(t("shawn@hotmail.com").toString()).snap( diff --git a/ark/type/parser/definition.ts b/ark/type/parser/definition.ts index c3ab2c7a0b..300da8d0a6 100644 --- a/ark/type/parser/definition.ts +++ b/ark/type/parser/definition.ts @@ -46,7 +46,7 @@ export const parseObject = (def: object, ctx: ParseContext): BaseRoot => { "intersection", { domain: "string", - regex: def as RegExp + pattern: def as RegExp }, { prereduced: true } ) diff --git a/ark/type/parser/string/shift/operand/enclosed.ts b/ark/type/parser/string/shift/operand/enclosed.ts index 3d083cd56d..d6a17a44e9 100644 --- a/ark/type/parser/string/shift/operand/enclosed.ts +++ b/ark/type/parser/string/shift/operand/enclosed.ts @@ -33,7 +33,7 @@ export const parseEnclosed = ( "intersection", { domain: "string", - regex: enclosed + pattern: enclosed }, { prereduced: true } ) diff --git a/ark/type/type.ts b/ark/type/type.ts index c7e2379e5b..a90cb6839d 100644 --- a/ark/type/type.ts +++ b/ark/type/type.ts @@ -15,10 +15,10 @@ import { type MorphAst, type NodeSchema, type Out, + type PatternSchema, type Predicate, type Prerequisite, type PrimitiveConstraintKind, - type RegexSchema, type Root, type ambient, type constrain, @@ -269,10 +269,10 @@ declare class _Type extends InnerRoot { schema: schema ): Type, $> - matching( - this: validateChainedConstraint<"regex", this>, + matching( + this: validateChainedConstraint<"pattern", this>, schema: schema - ): Type, $> + ): Type, $> atLeast( this: validateChainedConstraint<"min", this>, From 85e5558f25969a30df5ce32ff202cf84c89eec68 Mon Sep 17 00:00:00 2001 From: David Blass Date: Thu, 6 Jun 2024 16:55:31 -0400 Subject: [PATCH 04/61] add changeset --- .changeset/brave-plums-clap.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/brave-plums-clap.md diff --git a/.changeset/brave-plums-clap.md b/.changeset/brave-plums-clap.md new file mode 100644 index 0000000000..8a5ebca907 --- /dev/null +++ b/.changeset/brave-plums-clap.md @@ -0,0 +1,5 @@ +--- +"@arktype/schema": patch +--- + +Rename RegexNode to PatternNode From 886e643406f1256d1005af2e3ec7c184b3f3137c Mon Sep 17 00:00:00 2001 From: David Blass Date: Fri, 7 Jun 2024 09:04:48 -0400 Subject: [PATCH 05/61] bump deps --- package.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 71d36c3506..ea7c5d9a10 100644 --- a/package.json +++ b/package.json @@ -35,9 +35,9 @@ "@arktype/repo": "workspace:*", "@arktype/util": "workspace:*", "@changesets/changelog-github": "0.5.0", - "@changesets/cli": "2.27.1", - "@types/node": "20.12.12", - "prettier": "3.2.5", + "@changesets/cli": "2.27.5", + "@types/node": "20.14.2", + "prettier": "3.3.1", "eslint": "8.57.0", "eslint-config-prettier": "9.1.0", "eslint-define-config": "2.1.0", @@ -46,11 +46,11 @@ "eslint-plugin-mdx": "3.1.5", "eslint-plugin-only-warn": "1.1.0", "eslint-plugin-prefer-arrow-functions": "3.3.2", - "@typescript-eslint/eslint-plugin": "7.9.0", - "@typescript-eslint/parser": "7.9.0", + "@typescript-eslint/eslint-plugin": "7.12.0", + "@typescript-eslint/parser": "7.12.0", "c8": "9.1.0", - "knip": "5.16.0", - "tsx": "4.10.4", + "knip": "5.18.0", + "tsx": "4.13.2", "typescript": "5.4.5", "typescript-min": "npm:typescript@5.1.6", "typescript-nightly": "npm:typescript@next", @@ -59,7 +59,7 @@ }, "pnpm": { "overrides": { - "esbuild": "0.21.3" + "esbuild": "0.21.4" } }, "mocha": { From 00baf279a1105476959f203886c5f59c4a9a85de Mon Sep 17 00:00:00 2001 From: David Blass Date: Fri, 7 Jun 2024 14:14:04 -0400 Subject: [PATCH 06/61] fix morph with alias child --- ark/schema/inference.ts | 4 ++-- ark/schema/roots/morph.ts | 17 +++++++++-------- ark/type/__tests__/realWorld.test.ts | 17 +++++++++++++++++ 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/ark/schema/inference.ts b/ark/schema/inference.ts index b0e590c763..39971d8077 100644 --- a/ark/schema/inference.ts +++ b/ark/schema/inference.ts @@ -15,7 +15,7 @@ import type { DomainSchema } from "./roots/domain.js" import type { IntersectionSchema } from "./roots/intersection.js" import type { Morph, - MorphInputSchema, + MorphChildSchema, MorphSchema, Out, inferMorphOut @@ -75,7 +75,7 @@ type inferRootBranch = ) ? Out> : never - : schema extends MorphInputSchema ? inferMorphChild + : schema extends MorphChildSchema ? inferMorphChild : unknown type NonIntersectableBasisRoot = NonEnumerableDomain | Constructor | UnitSchema diff --git a/ark/schema/roots/morph.ts b/ark/schema/roots/morph.ts index ff16845afc..5ab6228ab0 100644 --- a/ark/schema/roots/morph.ts +++ b/ark/schema/roots/morph.ts @@ -33,18 +33,19 @@ import type { DefaultableAst } from "../structure/optional.js" import { BaseRoot, type Root, type schemaKindRightOf } from "./root.js" import { defineRightwardIntersections } from "./utils.js" -export type MorphInputKind = schemaKindRightOf<"morph"> +export type MorphChildKind = schemaKindRightOf<"morph"> | "alias" -const morphInputKinds: array = [ +const morphChildKinds: array = [ + "alias", "intersection", "unit", "domain", "proto" ] -export type MorphInputNode = Node +export type MorphChildNode = Node -export type MorphInputSchema = NodeSchema +export type MorphChildSchema = NodeSchema export type Morph = (In: i, ctx: TraversalContext) => o @@ -53,12 +54,12 @@ export type Out = ["=>", o] export type MorphAst = (In: i) => Out export interface MorphInner extends BaseMeta { - readonly in: MorphInputNode + readonly in: MorphChildNode readonly morphs: array } export interface MorphSchema extends BaseMeta { - readonly in: MorphInputSchema + readonly in: MorphChildSchema readonly morphs: listable } @@ -68,7 +69,7 @@ export interface MorphDeclaration schema: MorphSchema normalizedSchema: MorphSchema inner: MorphInner - childKind: MorphInputKind + childKind: MorphChildKind }> {} export const morphImplementation: nodeImplementationOf = @@ -78,7 +79,7 @@ export const morphImplementation: nodeImplementationOf = keys: { in: { child: true, - parse: (schema, ctx) => ctx.$.node(morphInputKinds, schema) + parse: (schema, ctx) => ctx.$.node(morphChildKinds, schema) }, morphs: { parse: arrayFrom, diff --git a/ark/type/__tests__/realWorld.test.ts b/ark/type/__tests__/realWorld.test.ts index 719c437528..dd479f8af5 100644 --- a/ark/type/__tests__/realWorld.test.ts +++ b/ark/type/__tests__/realWorld.test.ts @@ -516,4 +516,21 @@ nospace must be matched by ^\\S*$ (was "One space")`) 'box.box.box must be an object (was string) or must be null (was {"box":{"box":{"box":"whoops"}}})' ) }) + + it("morph with alias child", () => { + const types = scope({ + ArraySchema: { + "items?": "Schema" + }, + Schema: "TypeWithKeywords", + TypeWithKeywords: "ArraySchema" + }).export() + + const t = types.Schema.pipe(o => JSON.stringify(o)) + + attest(t({ items: {} })).snap('{"items":{}}') + attest(t({ items: null }).toString()).snap( + "items must be an object (was null)" + ) + }) }) From 2a94bcdd1a1b60182dd333f8868b278a50df920c Mon Sep 17 00:00:00 2001 From: David Blass Date: Fri, 7 Jun 2024 14:22:54 -0400 Subject: [PATCH 07/61] add changelogs --- .changeset/brave-plums-clap.md | 6 +++++- ark/type/CHANGELOG.md | 18 ++++++++++++++++++ ark/type/package.json | 2 +- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/.changeset/brave-plums-clap.md b/.changeset/brave-plums-clap.md index 8a5ebca907..15d5a61d97 100644 --- a/.changeset/brave-plums-clap.md +++ b/.changeset/brave-plums-clap.md @@ -2,4 +2,8 @@ "@arktype/schema": patch --- -Rename RegexNode to PatternNode +(see [arktype CHANGELOG](../type/CHANGELOG.md)) + +### Fix a ParseError compiling certain morphs with cyclic inputs + +### Rename RegexNode to PatternNode diff --git a/ark/type/CHANGELOG.md b/ark/type/CHANGELOG.md index 608ff2c5c8..cc0fc57afd 100644 --- a/ark/type/CHANGELOG.md +++ b/ark/type/CHANGELOG.md @@ -1,5 +1,23 @@ # arktype +## 2.0.0-dev.22 + +### Fix a ParseError compiling certain morphs with cyclic inputs + +Types like the following will now work: + +```ts +const types = scope({ + ArraySchema: { + "items?": "Schema" + }, + Schema: "TypeWithKeywords", + TypeWithKeywords: "ArraySchema" +}).export() + +const t = types.Schema.pipe(o => JSON.stringify(o)) +``` + ## 2.0.0-dev.21 ### Fix chained .describe() on union types diff --git a/ark/type/package.json b/ark/type/package.json index ec05b1cd08..980e5a74be 100644 --- a/ark/type/package.json +++ b/ark/type/package.json @@ -1,7 +1,7 @@ { "name": "arktype", "description": "TypeScript's 1:1 validator, optimized from editor to runtime", - "version": "2.0.0-dev.21", + "version": "2.0.0-dev.22", "license": "MIT", "author": { "name": "David Blass", From 98533ad8c0472e0a846d48adb26e79c0ba12b797 Mon Sep 17 00:00:00 2001 From: David Blass Date: Fri, 7 Jun 2024 17:12:10 -0400 Subject: [PATCH 08/61] cyclic scope --- .../content/docs/intro/adding-constraints.mdx | 23 ++++++++ .../content/docs/intro/your-first-type.mdx | 4 +- ark/docs/src/styles.css | 17 ++++-- ark/type/__tests__/scope.test.ts | 56 ++++++++++++++----- ark/type/api.ts | 7 ++- ark/type/scope.ts | 10 +++- 6 files changed, 94 insertions(+), 23 deletions(-) create mode 100644 ark/docs/src/content/docs/intro/adding-constraints.mdx diff --git a/ark/docs/src/content/docs/intro/adding-constraints.mdx b/ark/docs/src/content/docs/intro/adding-constraints.mdx new file mode 100644 index 0000000000..d3e246237d --- /dev/null +++ b/ark/docs/src/content/docs/intro/adding-constraints.mdx @@ -0,0 +1,23 @@ +--- +title: Adding Constraints +sidebar: + order: 3 +--- + +TypeScript is extremely versatile for representing types like `string` or `number`, but what about `email` or `positive integer less than 100`? + +In ArkType, conditions that narrow a type beyond its **basis** are called **constraints**. + +Constraints are a first-class citizen of ArkType and behave according to the same principles of set-theory that govern TypeScript. In other words, **they just work**. + +## Syntax + +```ts +import { type } from "arktype" +// ---cut--- +const contact = type({ + // many common + email: "email", + score: "1 <= integer < 100" +}) +``` diff --git a/ark/docs/src/content/docs/intro/your-first-type.mdx b/ark/docs/src/content/docs/intro/your-first-type.mdx index fbb086c10b..c277bdf8b5 100644 --- a/ark/docs/src/content/docs/intro/your-first-type.mdx +++ b/ark/docs/src/content/docs/intro/your-first-type.mdx @@ -6,10 +6,10 @@ sidebar: import { TypeBenchmarksGraph } from "../../../components/BenchmarksGraph.tsx" -## Define - If you already know TypeScript, congratulations- you just learned most of ArkType's syntax 🎉 +## Define + ```ts // @noErrors import { type } from "arktype" diff --git a/ark/docs/src/styles.css b/ark/docs/src/styles.css index d260b2745a..ad701ee15b 100644 --- a/ark/docs/src/styles.css +++ b/ark/docs/src/styles.css @@ -1,10 +1,19 @@ @import url("https://fonts.googleapis.com/css?family=Raleway:300,400,500,700&display=swap"); -@import url("https://fonts.cdnfonts.com/css/cascadia-code"); + +@font-face { + font-family: "Cascadia Mono"; + src: + local("Cascadia Mono"), + url("https://fonts.cdnfonts.com/s/37910/CascadiaMono.woff") format("woff"); + font-style: normal; +} /* Don't render italics, which can be hard to read for some users */ @font-face { - font-family: "Cascadia Code"; - src: local("Cascadia Code"); + font-family: "Cascadia Mono"; + src: + local("Cascadia Mono"), + url("https://fonts.cdnfonts.com/s/37910/CascadiaMono.woff") format("woff"); font-style: italic; } @@ -15,7 +24,7 @@ --hover-glow: 0.5rem 0.5rem 2rem 0 rgba(31, 38, 135, 0.37); /* Fonts */ --sl-font: Raleway, sans-serif; - --sl-font-mono: Cascadia Code; + --sl-font-mono: Cascadia Mono; /* Dark mode colors. */ --sl-color-accent-low: #4b3621; --sl-color-accent: #eb9f2e; diff --git a/ark/type/__tests__/scope.test.ts b/ark/type/__tests__/scope.test.ts index 7fc10fdcf2..1beedd3f0e 100644 --- a/ark/type/__tests__/scope.test.ts +++ b/ark/type/__tests__/scope.test.ts @@ -378,19 +378,49 @@ b.c.c must be arf&bork (was missing)`) ] }) }) + // https://github.com/arktypeio/arktype/issues/930 - // it("intersect cyclic reference with repeat name", () => { - // const types = scope({ - // arf: { - // b: "bork" - // }, - // bork: { - // c: "arf&bork" - // } - // }).export() - // attest(types.arf({ b: { c: {} } }).toString()) - // .snap(`b.c.b must be { c: arf&bork } (was missing) - // b.c.c must be arf&bork (was missing)`) - // }) + it("intersect cyclic reference with repeat name", () => { + const types = scope({ + arf: { + a: "bork" + }, + bork: { + b: "arf&bork" + } + }).export() + + const resolveRef: string = ( + types.bork.raw.firstReferenceOfKindOrThrow("alias").json as any + ).resolve + + attest(types.bork.json).snap({ + required: [ + { key: "b", value: { resolve: resolveRef, alias: "$arf&bork" } } + ], + domain: "object" + }) + + attest(types.arf.json).snap({ + required: [ + { + key: "a", + value: types.bork.json + } + ], + domain: "object" + }) + + attest(types.arf({ a: { b: {} } }).toString()) + .snap(`a.b.a must be { b: arf&bork } (was missing) +a.b.b must be arf&bork (was missing)`) + }) + }) + + it("can override ambient aliases", () => { + const $ = scope({ + bar: "Array", // Type for some reason? + Array: "string" + }).export() }) }) diff --git a/ark/type/api.ts b/ark/type/api.ts index 4934f1a18e..e35ac2f781 100644 --- a/ark/type/api.ts +++ b/ark/type/api.ts @@ -2,5 +2,10 @@ export { ArkError, ArkErrors as ArkErrors } from "@arktype/schema" export type { Ark, ArkConfig, Out } from "@arktype/schema" export { ambient, ark, declare, define, match, type } from "./ark.js" export { Module } from "./module.js" -export { scope, type Scope } from "./scope.js" +export { + scope, + type Scope, + type inferScope, + type validateScope +} from "./scope.js" export { Type, type inferTypeRoot, type validateTypeRoot } from "./type.js" diff --git a/ark/type/scope.ts b/ark/type/scope.ts index 17205038d7..a3fb63dd73 100644 --- a/ark/type/scope.ts +++ b/ark/type/scope.ts @@ -54,15 +54,17 @@ import { export type ScopeParser = ( def: validateScope, config?: ArkConfig -) => Scope>> +) => Scope> -type validateScope = { +export type validateScope = { [k in keyof def]: k extends symbol ? // this should only occur when importing/exporting modules, and those // keys should be ignored unknown : parseScopeKey["params"] extends [] ? - // Not including Type here directly breaks inference + // not including Type here directly breaks some cyclic tests (last checked w/ TS 5.5). + // if you are from the future with a better version of TS and can remove it + // without breaking `pnpm typecheck`, go for it. def[k] extends Type | PreparsedResolution ? def[k] : k extends PrivateDeclaration ? keyError> @@ -82,6 +84,8 @@ type validateScope = { > } +export type inferScope = inferBootstrapped> + export type bindThis = { this: Def } /** nominal type for an unparsed definition used during scope bootstrapping */ From ae9342ee279ab9c9ccf0432c3ca78ecb3214a95a Mon Sep 17 00:00:00 2001 From: David Blass Date: Fri, 7 Jun 2024 17:45:27 -0400 Subject: [PATCH 09/61] allow overriding aliases --- ark/schema/scope.ts | 18 ++++++++----- ark/type/__tests__/scope.test.ts | 25 +++++++++++++++--- ark/type/parser/semantic/infer.ts | 2 ++ .../parser/string/shift/operand/unenclosed.ts | 26 ++++++++++++++----- ark/type/parser/string/string.ts | 5 ++-- ark/type/scope.ts | 12 ++++++--- ark/type/type.ts | 11 +++----- 7 files changed, 68 insertions(+), 31 deletions(-) diff --git a/ark/schema/scope.ts b/ark/schema/scope.ts index c042d22f6a..08ff78e04e 100644 --- a/ark/schema/scope.ts +++ b/ark/schema/scope.ts @@ -225,7 +225,7 @@ export class RawRootScope<$ extends RawRootResolutions = RawRootResolutions> { readonly config: ArkConfig readonly resolvedConfig: ResolvedArkConfig - readonly id = `$${++scopeCount}`; + readonly id = `$${++scopeCount}` readonly [arkKind] = "scope" readonly referencesById: { [name: string]: BaseNode } = {} @@ -280,12 +280,16 @@ export class RawRootScope<$ extends RawRootResolutions = RawRootResolutions> // TODO: generics and modules this.resolutions = flatMorph( this.ambient.resolutions, - (alias, resolution) => [ - alias, - hasArkKind(resolution, "root") ? - resolution.bindScope(this) - : resolution - ] + (alias, resolution) => + // an alias defined in this scope should override an ambient alias of the same name + alias in this.aliases ? + [] + : [ + alias, + hasArkKind(resolution, "root") ? + resolution.bindScope(this) + : resolution + ] ) } scopesById[this.id] = this diff --git a/ark/type/__tests__/scope.test.ts b/ark/type/__tests__/scope.test.ts index 1beedd3f0e..1041e30639 100644 --- a/ark/type/__tests__/scope.test.ts +++ b/ark/type/__tests__/scope.test.ts @@ -1,9 +1,12 @@ import { attest, contextualize } from "@arktype/attest" import { + schema, writeUnboundableMessage, - writeUnresolvableMessage + writeUnresolvableMessage, + type string } from "@arktype/schema" import { define, scope, type } from "arktype" +import type { Module } from "../module.js" import { writeUnexpectedCharacterMessage } from "../parser/string/shift/operator/operator.js" contextualize(() => { @@ -418,9 +421,23 @@ a.b.b must be arf&bork (was missing)`) }) it("can override ambient aliases", () => { - const $ = scope({ - bar: "Array", // Type for some reason? - Array: "string" + const types = scope({ + foo: { + bar: "string" + }, + string: schema({ domain: "string" }).constrain("minLength", 1) }).export() + attest< + Module<{ + string: string.atLeastLength<1> + foo: { + bar: string.atLeastLength<1> + } + }> + >(types) + attest(types.foo.json).snap({ + required: [{ key: "bar", value: { domain: "string", minLength: 1 } }], + domain: "object" + }) }) }) diff --git a/ark/type/parser/semantic/infer.ts b/ark/type/parser/semantic/infer.ts index 68bcef210d..6b1c8caab8 100644 --- a/ark/type/parser/semantic/infer.ts +++ b/ark/type/parser/semantic/infer.ts @@ -4,6 +4,7 @@ import type { GenericProps, LimitLiteral, RegexLiteral, + ambient, constrain, distillIn, inferIntersection, @@ -130,6 +131,7 @@ export type InfixExpression< export type inferTerminal = token extends keyof args | keyof $ ? resolve + : token extends keyof ambient ? ambient[token] : `#${token}` extends keyof $ ? resolve<`#${token}`, $, args> : token extends StringLiteral ? text : token extends `${infer n extends number}` ? n diff --git a/ark/type/parser/string/shift/operand/unenclosed.ts b/ark/type/parser/string/shift/operand/unenclosed.ts index d3c5f221f8..a923211334 100644 --- a/ark/type/parser/string/shift/operand/unenclosed.ts +++ b/ark/type/parser/string/shift/operand/unenclosed.ts @@ -4,6 +4,7 @@ import { writeUnresolvableMessage, type GenericProps, type PrivateDeclaration, + type ambient, type arkKind, type writeNonSubmoduleDotMessage } from "@arktype/schema" @@ -134,21 +135,32 @@ const maybeParseUnenclosedLiteral = ( } type tryResolve = - token extends keyof $ ? token + token extends keyof ambient ? token + : token extends keyof $ ? token : `#${token}` extends keyof $ ? token : token extends keyof args ? token : token extends `${number}` ? token : token extends BigintLiteral ? token : token extends ( - `${infer submodule extends keyof $ & string}.${infer reference}` + `${infer submodule extends (keyof $ | keyof ambient) & string}.${infer reference}` ) ? - $[submodule] extends { [arkKind]: "module" } ? - reference extends keyof $[submodule] ? - token - : unresolvableError - : ErrorMessage> + tryResolveSubmodule : unresolvableError +type tryResolveSubmodule< + token, + submodule extends keyof $ & string, + reference extends string, + s extends StaticState, + $, + args +> = + $[submodule] extends { [arkKind]: "module" } ? + reference extends keyof $[submodule] ? + token + : unresolvableError + : ErrorMessage> + /** Provide valid completions for the current token, or fallback to an * unresolvable error if there are none */ export type unresolvableError< diff --git a/ark/type/parser/string/string.ts b/ark/type/parser/string/string.ts index 581baec858..4305f42f81 100644 --- a/ark/type/parser/string/string.ts +++ b/ark/type/parser/string/string.ts @@ -1,4 +1,4 @@ -import type { BaseRoot, resolvableReferenceIn } from "@arktype/schema" +import type { ambient, BaseRoot, resolvableReferenceIn } from "@arktype/schema" import { type ErrorMessage, throwInternalError, @@ -7,7 +7,7 @@ import { import type { inferAstRoot } from "../semantic/infer.js" import type { DynamicState, DynamicStateWithRoot } from "./reduce/dynamic.js" import type { StringifiablePrefixOperator } from "./reduce/shared.js" -import type { StaticState, state } from "./reduce/static.js" +import type { state, StaticState } from "./reduce/static.js" import type { parseOperand } from "./shift/operand/operand.js" import { type parseOperator, @@ -37,6 +37,7 @@ export type inferString = inferAstRoot< export type BaseCompletions<$, args, otherSuggestions extends string = never> = | resolvableReferenceIn<$> + | resolvableReferenceIn | (keyof args & string) | StringifiablePrefixOperator | otherSuggestions diff --git a/ark/type/scope.ts b/ark/type/scope.ts index a3fb63dd73..830898464e 100644 --- a/ark/type/scope.ts +++ b/ark/type/scope.ts @@ -68,14 +68,14 @@ export type validateScope = { def[k] extends Type | PreparsedResolution ? def[k] : k extends PrivateDeclaration ? keyError> - : validateDefinition, {}> + : validateDefinition, {}> : parseScopeKey["params"] extends GenericParamsParseError ? // use the full nominal type here to avoid an overlap between the // error message and a possible value for the property parseScopeKey["params"][0] : validateDefinition< def[k], - ambient & bootstrapAliases, + bootstrapAliases, { // once we support constraints on generic parameters, we'd use // the base type here: https://github.com/arktypeio/arktype/issues/796 @@ -116,7 +116,7 @@ type bootstrapAliases = { type inferBootstrapped<$> = show<{ [name in keyof $]: $[name] extends Def ? - inferDefinition + inferDefinition : $[name] extends GenericProps ? // add the scope in which the generic was defined here Generic @@ -151,6 +151,12 @@ export type tryInferSubmoduleReference<$, token> = subalias extends keyof $[submodule] ? $[submodule][subalias] : never + : token extends ( + `${infer submodule extends moduleKeyOf}.${infer subalias}` + ) ? + subalias extends keyof ambient[submodule] ? + ambient[submodule][subalias] + : never : never export interface ParseContext { diff --git a/ark/type/type.ts b/ark/type/type.ts index a90cb6839d..78282078f5 100644 --- a/ark/type/type.ts +++ b/ark/type/type.ts @@ -20,7 +20,6 @@ import { type Prerequisite, type PrimitiveConstraintKind, type Root, - type ambient, type constrain, type constraintKindOf, type distillIn, @@ -136,7 +135,7 @@ export class RawTypeParser extends Callable< export type DeclarationParser<$> = () => { // for some reason, making this a const parameter breaks preinferred validation type: ( - def: validateDeclared> + def: validateDeclared> ) => Type } @@ -357,12 +356,8 @@ export type DefinitionParser<$> = (def: validateTypeRoot) => def export type validateTypeRoot = validateDefinition< def, - $ & ambient, + $, bindThis > -export type inferTypeRoot = inferDefinition< - def, - $ & ambient, - bindThis -> +export type inferTypeRoot = inferDefinition> From a2ed10fc52406d626b8f1b7dd6554df2e718cab2 Mon Sep 17 00:00:00 2001 From: David Blass Date: Fri, 7 Jun 2024 18:08:31 -0400 Subject: [PATCH 10/61] add constraints to validation scope --- ark/schema/keywords/parsing.ts | 3 ++- ark/schema/keywords/validation.ts | 25 +++++++++++++------------ ark/type/CHANGELOG.md | 16 ++++++++++++++++ ark/type/__tests__/scope.test.ts | 3 ++- 4 files changed, 33 insertions(+), 14 deletions(-) diff --git a/ark/schema/keywords/parsing.ts b/ark/schema/keywords/parsing.ts index 24545298c5..2da60b39d9 100644 --- a/ark/schema/keywords/parsing.ts +++ b/ark/schema/keywords/parsing.ts @@ -3,6 +3,7 @@ import { wellFormedIntegerMatcher, wellFormedNumberMatcher } from "@arktype/util" +import type { number } from "../ast.js" import type { SchemaModule } from "../module.js" import type { Out } from "../roots/morph.js" import { root, schemaScope } from "../scope.js" @@ -62,7 +63,7 @@ const date = root.defineRoot({ export type parsingExports = { url: (In: string) => Out number: (In: string) => Out - integer: (In: string) => Out + integer: (In: string) => Out> date: (In: string) => Out json: (In: string) => Out } diff --git a/ark/schema/keywords/validation.ts b/ark/schema/keywords/validation.ts index 5e5655da6d..bac14b3dd5 100644 --- a/ark/schema/keywords/validation.ts +++ b/ark/schema/keywords/validation.ts @@ -1,3 +1,4 @@ +import type { number, string } from "../ast.js" import type { SchemaModule } from "../module.js" import { root, schemaScope } from "../scope.js" import { creditCardMatcher, isLuhnValid } from "./utils/creditCard.js" @@ -54,18 +55,18 @@ const creditCard = root.defineRoot({ }) export interface validationExports { - alpha: string - alphanumeric: string - digits: string - lowercase: string - uppercase: string - creditCard: string - email: string - uuid: string - url: string - semver: string - ip: string - integer: number + alpha: string.matching + alphanumeric: string.matching + digits: string.matching + lowercase: string.matching + uppercase: string.matching + creditCard: string.matching + email: string.matching + uuid: string.matching + url: string.matching + semver: string.matching + ip: string.matching + integer: number.divisibleBy<1> } export type validation = SchemaModule diff --git a/ark/type/CHANGELOG.md b/ark/type/CHANGELOG.md index cc0fc57afd..dcbf9dae36 100644 --- a/ark/type/CHANGELOG.md +++ b/ark/type/CHANGELOG.md @@ -2,6 +2,22 @@ ## 2.0.0-dev.22 +### Allow overriding builtin keywords + +```ts +// all references to string in this scope now enforce minLength: 1 +const $ = scope({ + foo: { + // has minLength: 1 + bar: "string" + }, + string: schema({ domain: "string" }).constrain("minLength", 1) +}) + +// has minLength: 1 +const s = $.type("string") +``` + ### Fix a ParseError compiling certain morphs with cyclic inputs Types like the following will now work: diff --git a/ark/type/__tests__/scope.test.ts b/ark/type/__tests__/scope.test.ts index 1041e30639..ecb5aa23e4 100644 --- a/ark/type/__tests__/scope.test.ts +++ b/ark/type/__tests__/scope.test.ts @@ -3,6 +3,7 @@ import { schema, writeUnboundableMessage, writeUnresolvableMessage, + type distillOut, type string } from "@arktype/schema" import { define, scope, type } from "arktype" @@ -217,7 +218,7 @@ contextualize(() => { } }) - type Package = ReturnType["t"]["package"] + type Package = distillOut["t"]["package"]> const getCyclicData = () => { const packageData = { From 170bc6e6a4d2504ce4bbb467f7e083de048c482e Mon Sep 17 00:00:00 2001 From: David Blass Date: Fri, 7 Jun 2024 23:13:57 -0400 Subject: [PATCH 11/61] remove constraints type parameter constraints --- ark/schema/ast.ts | 190 ++++++++++++++---------------- ark/schema/keywords/validation.ts | 24 ++-- 2 files changed, 99 insertions(+), 115 deletions(-) diff --git a/ark/schema/ast.ts b/ark/schema/ast.ts index 2ae5f13b4e..fee2043fd5 100644 --- a/ark/schema/ast.ts +++ b/ark/schema/ast.ts @@ -12,15 +12,9 @@ export type DateLiteral = | `d"${source}"` | `d'${source}'` -export type Constraints = { - divisor?: { [k: number]: 1 } - min?: { [k: number | string]: 0 | 1 } - max?: { [k: number | string]: 0 | 1 } - pattern?: { [k: string]: 1 } - length?: { [k: number]: 1 } - predicate?: 1 - literal?: string | number -} +export type ConstraintSet = Record + +export type Constraints = Record export declare const constrained: unique symbol @@ -37,40 +31,48 @@ export type LimitLiteral = number | DateLiteral export type normalizeLimit = limit extends DateLiteral ? source : limit extends number | string ? limit - : string + : never + +type constraint = { [k in rule & PropertyKey]: 1 } -export type AtLeast = { - min: { [k in rule]: 0 | 1 } +export type AtLeast = { + atLeast: constraint } -export type AtMost = { - max: { [k in rule]: 0 | 1 } +export type AtMost = { + atMost: constraint } -export type MoreThan = { - min: { [k in rule]: 0 } +export type MoreThan = { + moreThan: constraint } -export type LessThan = { - max: { [k in rule]: 0 } +export type LessThan = { + lessThan: constraint } -export type Literal = { literal: rule } +export type Literal = { + literal: constraint +} -export type DivisibleBy = { - divisor: { [k in rule]: 1 } +export type DivisibleBy = { + divisibleBy: constraint } -export type Length = { - length: { [k in rule]: 1 } +export type Length = { + length: constraint } -export type Matching = { - pattern: { [k in rule]: 1 } +export type Matching = { + matching: constraint } +export declare const anonymous: unique symbol + +export type anonymous = typeof anonymous + export type Narrowed = { - predicate: 1 + predicate: { [anonymous]: 1 } } export type primitiveConstraintKindOf = Extract< @@ -79,15 +81,15 @@ export type primitiveConstraintKindOf = Extract< > export namespace number { - export type atLeast = of> + export type atLeast = of> - export type moreThan = of> + export type moreThan = of> - export type atMost = of> + export type atMost = of> - export type lessThan = of> + export type lessThan = of> - export type divisibleBy = of> + export type divisibleBy = of> export type narrowed = of @@ -100,62 +102,50 @@ export namespace number { normalizePrimitiveConstraintRoot extends infer rule ? kind extends "min" ? schema extends { exclusive: true } ? - moreThan - : atLeast + moreThan + : atLeast : kind extends "max" ? schema extends { exclusive: true } ? - lessThan - : atMost - : kind extends "divisor" ? divisibleBy + lessThan + : atMost + : kind extends "divisor" ? divisibleBy : narrowed : never } -export type AtLeastLength = { - min: { [k in rule]: 0 | 1 } +export type AtLeastLength = { + atLeastLength: constraint } -export type AtMostLength = { - max: { [k in rule]: 0 | 1 } +export type AtMostLength = { + atMostLength: constraint } -export type MoreThanLength = { - min: { [k in rule]: 0 } +export type MoreThanLength = { + moreThanLength: constraint } -export type LessThanLength = { - max: { [k in rule]: 0 } +export type LessThanLength = { + lessThanLength: constraint } -export type ExactlyLength = { - min: { [k in rule]: 1 } - max: { [k in rule]: 1 } +export type ExactlyLength = { + atLeastLength: constraint + atMostLength: constraint } export namespace string { - export type atLeastLength = of< - string, - AtLeastLength - > + export type atLeastLength = of> - export type moreThanLength = of< - string, - MoreThanLength - > + export type moreThanLength = of> - export type atMostLength = of> + export type atMostLength = of> - export type lessThanLength = of< - string, - LessThanLength - > + export type lessThanLength = of> - export type exactlyLength = of< - string, - ExactlyLength - > + export type exactlyLength = of> - export type matching = of> + export type matching = of> export type narrowed = of @@ -168,52 +158,46 @@ export namespace string { normalizePrimitiveConstraintRoot extends infer rule ? kind extends "minLength" ? schema extends { exclusive: true } ? - moreThanLength - : atLeastLength + moreThanLength + : atLeastLength : kind extends "maxLength" ? schema extends { exclusive: true } ? - lessThanLength - : atMostLength + lessThanLength + : atMostLength : kind extends "pattern" ? matching - : kind extends "exactLength" ? exactlyLength + : kind extends "exactLength" ? exactlyLength : narrowed : never } -export type AtOrAfter = { - min: { [k in rule]: 0 | 1 } +export type AtOrAfter = { + atOrAfter: constraint } -export type AtOrBefore = { - max: { [k in rule]: 0 | 1 } +export type AtOrBefore = { + atOrBefore: constraint } -export type After = { - min: { [k in rule]: 0 } +export type After = { + after: constraint } -export type Before = { - max: { [k in rule]: 0 } +export type Before = { + before: constraint } export namespace Date { - export type atOrAfter = of< - Date, - AtOrAfter - > + export type atOrAfter = of> - export type after = of> + export type after = of> - export type atOrBefore = of< - Date, - AtOrBefore - > + export type atOrBefore = of> - export type before = of> + export type before = of> export type narrowed = of - export type literal = of> + export type literal = of> export type is = of @@ -265,34 +249,34 @@ type _constrain< export type normalizePrimitiveConstraintRoot< schema extends NodeSchema > = - "rule" extends keyof schema ? conform - : conform + "rule" extends keyof schema ? conform + : conform export type schemaToConstraint< kind extends PrimitiveConstraintKind, schema extends NodeSchema > = normalizePrimitiveConstraintRoot extends infer rule ? - kind extends "pattern" ? Matching - : kind extends "divisor" ? DivisibleBy - : kind extends "exactLength" ? Length + kind extends "pattern" ? Matching + : kind extends "divisor" ? DivisibleBy + : kind extends "exactLength" ? Length : kind extends "min" ? schema extends { exclusive: true } ? - MoreThan - : AtLeast + MoreThan + : AtLeast : kind extends "max" ? schema extends { exclusive: true } ? - LessThan - : AtMost + LessThan + : AtMost : kind extends "minLength" ? schema extends { exclusive: true } ? - MoreThanLength - : AtLeastLength + MoreThanLength + : AtLeastLength : kind extends "maxLength" ? schema extends { exclusive: true } ? - LessThanLength - : AtMostLength - : kind extends "exactLength" ? ExactlyLength + LessThanLength + : AtMostLength + : kind extends "exactLength" ? ExactlyLength : kind extends "after" ? schema extends { exclusive: true } ? After> diff --git a/ark/schema/keywords/validation.ts b/ark/schema/keywords/validation.ts index bac14b3dd5..fed445aa84 100644 --- a/ark/schema/keywords/validation.ts +++ b/ark/schema/keywords/validation.ts @@ -1,4 +1,4 @@ -import type { number, string } from "../ast.js" +import type { anonymous, number, string } from "../ast.js" import type { SchemaModule } from "../module.js" import { root, schemaScope } from "../scope.js" import { creditCardMatcher, isLuhnValid } from "./utils/creditCard.js" @@ -55,17 +55,17 @@ const creditCard = root.defineRoot({ }) export interface validationExports { - alpha: string.matching - alphanumeric: string.matching - digits: string.matching - lowercase: string.matching - uppercase: string.matching - creditCard: string.matching - email: string.matching - uuid: string.matching - url: string.matching - semver: string.matching - ip: string.matching + alpha: string.matching + alphanumeric: string.matching + digits: string.matching + lowercase: string.matching + uppercase: string.matching + creditCard: string.matching + email: string.matching + uuid: string.matching + url: string.matching + semver: string.matching + ip: string.matching integer: number.divisibleBy<1> } From 9d0bf28c5b31c197a184e66a4ce05368156062bc Mon Sep 17 00:00:00 2001 From: David Blass Date: Fri, 7 Jun 2024 23:33:01 -0400 Subject: [PATCH 12/61] remove broken cyclic rereferences test --- ark/type/__tests__/scope.test.ts | 72 ++++++++++++++++---------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/ark/type/__tests__/scope.test.ts b/ark/type/__tests__/scope.test.ts index ecb5aa23e4..04468f1372 100644 --- a/ark/type/__tests__/scope.test.ts +++ b/ark/type/__tests__/scope.test.ts @@ -383,42 +383,42 @@ b.c.c must be arf&bork (was missing)`) }) }) - // https://github.com/arktypeio/arktype/issues/930 - it("intersect cyclic reference with repeat name", () => { - const types = scope({ - arf: { - a: "bork" - }, - bork: { - b: "arf&bork" - } - }).export() - - const resolveRef: string = ( - types.bork.raw.firstReferenceOfKindOrThrow("alias").json as any - ).resolve - - attest(types.bork.json).snap({ - required: [ - { key: "b", value: { resolve: resolveRef, alias: "$arf&bork" } } - ], - domain: "object" - }) - - attest(types.arf.json).snap({ - required: [ - { - key: "a", - value: types.bork.json - } - ], - domain: "object" - }) - - attest(types.arf({ a: { b: {} } }).toString()) - .snap(`a.b.a must be { b: arf&bork } (was missing) -a.b.b must be arf&bork (was missing)`) - }) + // // https://github.com/arktypeio/arktype/issues/930 + // it("intersect cyclic reference with repeat name", () => { + // const types = scope({ + // arf: { + // a: "bork" + // }, + // bork: { + // b: "arf&bork" + // } + // }).export() + + // const resolveRef: string = ( + // types.bork.raw.firstReferenceOfKindOrThrow("alias").json as any + // ).resolve + + // attest(types.bork.json).snap({ + // required: [ + // { key: "b", value: { resolve: resolveRef, alias: "$arf&bork" } } + // ], + // domain: "object" + // }) + + // attest(types.arf.json).snap({ + // required: [ + // { + // key: "a", + // value: types.bork.json + // } + // ], + // domain: "object" + // }) + + // attest(types.arf({ a: { b: {} } }).toString()) + // .snap(`a.b.a must be { b: arf&bork } (was missing) + // a.b.b must be arf&bork (was missing)`) + // }) }) it("can override ambient aliases", () => { From 8628a77f4e68898638986754e43e33186ff5e0e7 Mon Sep 17 00:00:00 2001 From: David Blass Date: Fri, 7 Jun 2024 23:56:15 -0400 Subject: [PATCH 13/61] cleanup anonymous constraint display --- ark/schema/ast.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/ark/schema/ast.ts b/ark/schema/ast.ts index fee2043fd5..02fb82daf0 100644 --- a/ark/schema/ast.ts +++ b/ark/schema/ast.ts @@ -67,12 +67,10 @@ export type Matching = { matching: constraint } -export declare const anonymous: unique symbol - -export type anonymous = typeof anonymous +export type anonymous = "?" export type Narrowed = { - predicate: { [anonymous]: 1 } + predicate: { [k in anonymous]: 1 } } export type primitiveConstraintKindOf = Extract< From 4c4e61fa2eee66d433c1c767a8a54213c368a893 Mon Sep 17 00:00:00 2001 From: Izan Robinson Date: Mon, 23 Sep 2024 08:58:49 +0100 Subject: [PATCH 14/61] Export from ark/schema/shared/traversal.ts so can import TraversalContext type --- ark/schema/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/ark/schema/index.ts b/ark/schema/index.ts index 7c2f4bbf12..d53a8a9d77 100644 --- a/ark/schema/index.ts +++ b/ark/schema/index.ts @@ -33,6 +33,7 @@ export * from "./shared/implement.ts" export * from "./shared/intersections.ts" export * from "./shared/jsonSchema.ts" export * from "./shared/registry.ts" +export * from "./shared/traversal.ts" export * from "./shared/utils.ts" export * from "./structure/index.ts" export * from "./structure/optional.ts" From 934cd3698659dcd5643816158002d9d4a97cc271 Mon Sep 17 00:00:00 2001 From: Izan Robinson Date: Mon, 23 Sep 2024 09:01:26 +0100 Subject: [PATCH 15/61] Add parsing & types for array JSON Schema (w/ excessively deep type error) --- ark/jsonschema/array.ts | 164 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 ark/jsonschema/array.ts diff --git a/ark/jsonschema/array.ts b/ark/jsonschema/array.ts new file mode 100644 index 0000000000..36aa05dd86 --- /dev/null +++ b/ark/jsonschema/array.ts @@ -0,0 +1,164 @@ +import { + rootSchema, + type Intersection, + type Predicate, + type TraversalContext +} from "@ark/schema" +import { printable, type array } from "@ark/util" +import type { Type, applyConstraint, schemaToConstraint } from "arktype" + +import { innerParseJsonSchema, type inferJsonSchema } from "./json.js" +import { JsonSchema } from "./scope.js" + +const deepNormalize = (data: unknown): unknown => + typeof data === "object" ? + data === null ? null + : Array.isArray(data) ? data.map(item => deepNormalize(item)) + : Object.fromEntries( + Object.entries(data) + .map(([k, v]) => [k, deepNormalize(v)] as const) + .sort((l, r) => (l[0] > r[0] ? 1 : -1)) + ) + : data + +const arrayItemsAreUnique = ( + array: readonly unknown[], + ctx: TraversalContext +) => { + const seen: Record = {} + const duplicates: unknown[] = [] + for (const item of array) { + const stringified = JSON.stringify(deepNormalize(item)) + if (stringified in seen) duplicates.push(item) + else seen[stringified] = true + } + return duplicates.length === 0 ? + true + : ctx.reject({ + expected: "unique array items", + actual: `duplicated at elements ${printable(duplicates)}` + }) +} + +const arrayContainsItemMatchingSchema = ( + array: readonly unknown[], + schema: Type, + ctx: TraversalContext +) => + array.some(item => schema.allows(item)) === true ? + true + : ctx.mustBe( + "an array containing at least one item matching 'contains' schema" + ) + +export const validateJsonSchemaArray = JsonSchema.ArraySchema.pipe( + jsonSchema => { + const arktypeArraySchema: Intersection.Schema> = { + proto: "Array" + } + + if ("items" in jsonSchema) { + if (Array.isArray(jsonSchema.items)) { + arktypeArraySchema.sequence = { + prefix: jsonSchema.items.map( + item => innerParseJsonSchema.assert(item).internal + ) + } + + if ("additionalItems" in jsonSchema) { + if (jsonSchema.additionalItems === false) + arktypeArraySchema.exactLength = jsonSchema.items.length + else { + arktypeArraySchema.sequence = { + ...arktypeArraySchema.sequence, + variadic: innerParseJsonSchema.assert(jsonSchema.additionalItems) + .internal + } + } + } + } else { + arktypeArraySchema.sequence = { + variadic: innerParseJsonSchema.assert(jsonSchema.items).internal + } + } + } + + if ("maxItems" in jsonSchema) + arktypeArraySchema.maxLength = jsonSchema.maxItems + if ("minItems" in jsonSchema) + arktypeArraySchema.minLength = jsonSchema.minItems + + const predicates: Predicate.Schema[] = [] + if ("uniqueItems" in jsonSchema && jsonSchema.uniqueItems === true) + predicates.push((arr: unknown[], ctx) => arrayItemsAreUnique(arr, ctx)) + + if ("contains" in jsonSchema) { + const parsedContainsJsonSchema = innerParseJsonSchema.assert( + jsonSchema.contains + ) + predicates.push((arr: unknown[], ctx) => + arrayContainsItemMatchingSchema(arr, parsedContainsJsonSchema, ctx) + ) + } + + arktypeArraySchema.predicate = predicates + + return rootSchema(arktypeArraySchema) as unknown as Type + } +) + +type inferArrayOfJsonSchema> = { + [index in keyof tuple]: inferJsonSchema +} + +export type inferJsonSchemaArray = + "additionalItems" extends keyof arraySchema ? + "items" extends keyof arraySchema ? + arraySchema["items"] extends array ? + inferJsonSchemaArrayConstraints< + Omit, + [ + ...inferJsonSchemaArrayItems, + ...inferJsonSchema[] + ] + > + : // JSON Schema spec explicitly says that additionalItems MUST be ignored if items is not an array, and it's NOT an error + inferJsonSchemaArray, T> + : inferJsonSchema + : "items" extends keyof arraySchema ? + inferJsonSchemaArray< + Omit, + T & inferJsonSchemaArrayItems + > + : inferJsonSchemaArrayConstraints + +type inferJsonSchemaArrayConstraints = + "maxItems" extends keyof arraySchema ? + inferJsonSchemaArrayConstraints< + Omit, + applyConstraint< + T, + schemaToConstraint<"maxLength", arraySchema["maxItems"] & number> + > + > + : "minItems" extends keyof arraySchema ? + inferJsonSchemaArrayConstraints< + Omit, + applyConstraint< + T, + schemaToConstraint<"minLength", arraySchema["minItems"] & number> + > + > + : T extends {} ? T + : never + +type inferJsonSchemaArrayItems = + arrayItemsSchema extends array ? + arrayItemsSchema["length"] extends 0 ? + // JSON Schema explicitly states that {items: []} means "an array of anything" + // https://json-schema.org/understanding-json-schema/reference/array#items + unknown[] + : arrayItemsSchema extends array ? + inferArrayOfJsonSchema + : never + : inferJsonSchema[] From d3bc90db07c0f96e6ce0c4e1e8c7ebf194b61986 Mon Sep 17 00:00:00 2001 From: Izan Robinson Date: Mon, 23 Sep 2024 09:02:07 +0100 Subject: [PATCH 16/61] Add parsing & types for number/integer JSON Schema --- ark/jsonschema/number.ts | 81 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 ark/jsonschema/number.ts diff --git a/ark/jsonschema/number.ts b/ark/jsonschema/number.ts new file mode 100644 index 0000000000..9da6eb9194 --- /dev/null +++ b/ark/jsonschema/number.ts @@ -0,0 +1,81 @@ +import { rootSchema, type Intersection } from "@ark/schema" +import { throwParseError } from "@ark/util" +import type { Type, number } from "arktype" +import { JsonSchema } from "./scope.js" + +export const validateJsonSchemaNumber = JsonSchema.NumberSchema.pipe( + jsonSchema => { + const arktypeNumberSchema: Intersection.Schema = { + domain: "number" + } + + if ("maximum" in jsonSchema) { + if ("exclusiveMaximum" in jsonSchema) { + throwParseError( + "Provided number JSON Schema cannot have 'maximum' and 'exclusiveMaximum" + ) + } + arktypeNumberSchema.max = jsonSchema.maximum + } else if ("exclusiveMaximum" in jsonSchema) { + arktypeNumberSchema.max = { + rule: jsonSchema.exclusiveMaximum, + exclusive: true + } + } + + if ("minimum" in jsonSchema) { + if ("exclusiveMinimum" in jsonSchema) { + throwParseError( + "Provided number JSON Schema cannot have 'minimum' and 'exclusiveMinimum" + ) + } + arktypeNumberSchema.min = jsonSchema.minimum + } else if ("exclusiveMinimum" in jsonSchema) { + arktypeNumberSchema.min = { + rule: jsonSchema.exclusiveMinimum, + exclusive: true + } + } + + if ("multipleOf" in jsonSchema) + arktypeNumberSchema.divisor = jsonSchema.multipleOf + else if (jsonSchema.type === "integer") arktypeNumberSchema.divisor = 1 + + return rootSchema(arktypeNumberSchema) as unknown as Type + } +) + +export type inferJsonSchemaNumber = + "exclusiveMaximum" extends keyof numberSchema ? + inferJsonSchemaNumber< + Omit, + T & number.lessThan + > + : "exclusiveMinimum" extends keyof numberSchema ? + inferJsonSchemaNumber< + Omit, + T & number.moreThan + > + : "maximum" extends keyof numberSchema ? + inferJsonSchemaNumber< + Omit, + T & number.atMost + > + : "minimum" extends keyof numberSchema ? + inferJsonSchemaNumber< + Omit, + T & number.atLeast + > + : "multipleOf" extends keyof numberSchema ? + inferJsonSchemaNumber< + Omit & { type: "number" }, + T & number.divisibleBy + > + : "type" extends keyof numberSchema ? + numberSchema["type"] extends "integer" ? + inferJsonSchemaNumber< + Omit & { type: "number" }, + T & number.divisibleBy<1> + > + : T + : never // TODO: Throw type error (must have {type: "number"|"integer"} ) From da95a377abb5d474dc984d851fd136e07a77846e Mon Sep 17 00:00:00 2001 From: Izan Robinson Date: Mon, 23 Sep 2024 09:02:42 +0100 Subject: [PATCH 17/61] Add parsing & types for string JSON Schema --- ark/jsonschema/string.ts | 43 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 ark/jsonschema/string.ts diff --git a/ark/jsonschema/string.ts b/ark/jsonschema/string.ts new file mode 100644 index 0000000000..ad2ec33ae7 --- /dev/null +++ b/ark/jsonschema/string.ts @@ -0,0 +1,43 @@ +import { rootSchema, type Intersection } from "@ark/schema" +import type { Type, string } from "arktype" +import { JsonSchema } from "./scope.js" + +export const validateJsonSchemaString = JsonSchema.StringSchema.pipe( + jsonSchema => { + const arktypeStringSchema: Intersection.Schema = { + domain: "string" + } + + if ("maxLength" in jsonSchema) + arktypeStringSchema.maxLength = jsonSchema.maxLength + if ("minLength" in jsonSchema) + arktypeStringSchema.minLength = jsonSchema.minLength + if ("pattern" in jsonSchema) { + if (jsonSchema.pattern instanceof RegExp) { + arktypeStringSchema.pattern = [ + // Strip leading and trailing slashes from RegExp + jsonSchema.pattern.toString().slice(1, -1) + ] + } else arktypeStringSchema.pattern = [jsonSchema.pattern] + } + return rootSchema(arktypeStringSchema) as unknown as Type + } +) + +export type inferJsonSchemaString = + "maxLength" extends keyof stringSchema ? + inferJsonSchemaString< + Omit, + T & string.atMostLength + > + : "minLength" extends keyof stringSchema ? + inferJsonSchemaString< + Omit, + T & string.atLeastLength + > + : "pattern" extends keyof stringSchema ? + inferJsonSchemaString< + Omit, + T & string.matching + > + : T From 4d230c1328b9f8acce8e2c6a7cfc90ec51562e5a Mon Sep 17 00:00:00 2001 From: Izan Robinson Date: Mon, 23 Sep 2024 09:03:12 +0100 Subject: [PATCH 18/61] Add ArkType Scope representing JSON Schema schemas --- ark/jsonschema/scope.ts | 79 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 ark/jsonschema/scope.ts diff --git a/ark/jsonschema/scope.ts b/ark/jsonschema/scope.ts new file mode 100644 index 0000000000..87f6cf8810 --- /dev/null +++ b/ark/jsonschema/scope.ts @@ -0,0 +1,79 @@ +import { scope } from "arktype" + +const $ = scope({ + AnyKeywords: { + "const?": "unknown", + "enum?": "unknown[]" + // "type?": "string" + }, + CompositionKeywords: { + "allOf?": "Schema[]", + "anyOf?": "Schema[]", + "oneOf?": "Schema[]", + "not?": "Schema" + }, + TypeWithNoKeywords: { type: "'boolean'|'null'" }, + TypeWithKeywords: "ArraySchema|NumberSchema|ObjectSchema|StringSchema", + // NB: For sake of simplicitly, at runtime it's assumed that + // whatever we're parsing is valid JSON since it will be 99% of the time. + // This decision may be changed later, e.g. when a built-in JSON type exists in AT. + Json: "unknown", + "#BaseSchema": + // NB: `true` means "accept an valid JSON"; `false` means "reject everything". + "boolean|TypeWithNoKeywords|TypeWithKeywords|AnyKeywords|CompositionKeywords", + Schema: "BaseSchema|BaseSchema[]", + ArraySchema: { + "additionalItems?": "Schema", + "contains?": "Schema", + // JSON Schema states that if 'items' is not present, then treat as an empty schema (i.e. accept any valid JSON) + "items?": "Schema|Schema[]", + "maxItems?": "number.integer>=0", + "minItems?": "number.integer>=0", + type: "'array'", + "uniqueItems?": "boolean" + }, + NumberSchema: { + // NB: Technically 'exclusiveMaximum' and 'exclusiveMinimum' are mutually exclusive with 'maximum' and 'minimum', respectively, + // which is reflected at runtime but it's not worth the performance cost to validate this statically. + "exclusiveMaximum?": "number", + "exclusiveMinimum?": "number", + "maximum?": "number", + "minimum?": "number", + // NB: JSON Schema allows decimal multipleOf, but ArkType only supports integer. + "multipleOf?": "number.integer", + type: "'number'|'integer'" + }, + ObjectSchema: { + "additionalProperties?": "Schema", + "maxProperties?": "number.integer>=0", + "minProperties?": "number.integer>=0", + "patternProperties?": { "[string]": "Schema" }, + // NB: Technically 'properties' is required when 'required' is present, + // which is reflected at runtime but it's not worth the performance cost to validate this statically. + "properties?": { "[string]": "Schema" }, + "propertyNames?": "Schema", + "required?": "string[]", + type: "'object'" + }, + StringSchema: { + "maxLength?": "number.integer>=0", + "minLength?": "number.integer>=0", + "pattern?": "RegExp | string", + type: "'string'" + } +}) +export const JsonSchema = $.export() + +export declare namespace JsonSchema { + export type $ = typeof $ + export type Schema = typeof JsonSchema.Schema.infer + export type Json = typeof JsonSchema.Json.infer + export type AnyKeywords = typeof JsonSchema.AnyKeywords.infer + export type CompositionKeywords = typeof JsonSchema.CompositionKeywords.infer + export type TypeWithKeywords = typeof JsonSchema.TypeWithKeywords.infer + export type TypeWithNoKeywords = typeof JsonSchema.TypeWithNoKeywords.infer + export type ArraySchema = typeof JsonSchema.ArraySchema.infer + export type NumberSchema = typeof JsonSchema.NumberSchema.infer + export type ObjectSchema = typeof JsonSchema.ObjectSchema.infer + export type StringSchema = typeof JsonSchema.StringSchema.infer +} From 48a654a28364a354f1e5655e18cfc93caa4f2196 Mon Sep 17 00:00:00 2001 From: Izan Robinson Date: Mon, 23 Sep 2024 09:04:38 +0100 Subject: [PATCH 19/61] Initialise ark/jsonschema package --- ark/jsonschema/CHANGELOG.md | 12 ++++++++++ ark/jsonschema/README.md | 1 + ark/jsonschema/package.json | 35 ++++++++++++++++++++++++++++++ ark/jsonschema/tsconfig.build.json | 1 + 4 files changed, 49 insertions(+) create mode 100644 ark/jsonschema/CHANGELOG.md create mode 100644 ark/jsonschema/README.md create mode 100644 ark/jsonschema/package.json create mode 120000 ark/jsonschema/tsconfig.build.json diff --git a/ark/jsonschema/CHANGELOG.md b/ark/jsonschema/CHANGELOG.md new file mode 100644 index 0000000000..a8a11d4cff --- /dev/null +++ b/ark/jsonschema/CHANGELOG.md @@ -0,0 +1,12 @@ +# @arktype/jsonschema + +## 1.0.0 + +### Initial Release + +Released the initial implementation of the package. + +Known limitations: +- No `dependencies` support +- No `if`/`else`/`then` support +- `multipleOf` only supports integers \ No newline at end of file diff --git a/ark/jsonschema/README.md b/ark/jsonschema/README.md new file mode 100644 index 0000000000..8d041f79a9 --- /dev/null +++ b/ark/jsonschema/README.md @@ -0,0 +1 @@ +# @arktype/jsonschema diff --git a/ark/jsonschema/package.json b/ark/jsonschema/package.json new file mode 100644 index 0000000000..cc944ff40d --- /dev/null +++ b/ark/jsonschema/package.json @@ -0,0 +1,35 @@ +{ + "name": "@ark/jsonschema", + "version": "1.0.0", + "license": "MIT", + "author": { + "name": "TizzySaurus", + "email": "tizzysaurus@gmail.com", + "url": "https://github.com/tizzysaurus" + }, + "repository": { + "type": "git", + "url": "https://github.com/arktypeio/arktype.git" + }, + "type": "module", + "main": "./out/api.js", + "types": "./out/api.d.ts", + "exports": { + ".": "./out/api.js", + "./internal/*": "./out/*" + }, + "files": [ + "out" + ], + "scripts": { + "build": "tsx ../repo/build.ts", + "bench": "tsx ./__tests__/comparison.bench.ts", + "test": "tsx ../repo/testPackage.ts", + "tnt": "tsx ../repo/testPackage.ts --skipTypes" + }, + "dependencies": { + "arktype": "workspace:*", + "@ark/schema": "workspace:*", + "@ark/util": "workspace:*" + } +} diff --git a/ark/jsonschema/tsconfig.build.json b/ark/jsonschema/tsconfig.build.json new file mode 120000 index 0000000000..f74ef64d4c --- /dev/null +++ b/ark/jsonschema/tsconfig.build.json @@ -0,0 +1 @@ +../repo/tsconfig.esm.json \ No newline at end of file From b343bfe5dee68a56d28755608a66873a047fbf6a Mon Sep 17 00:00:00 2001 From: Izan Robinson Date: Mon, 23 Sep 2024 09:05:11 +0100 Subject: [PATCH 20/61] Add parsing & types for object JSON Schema --- ark/jsonschema/object.ts | 336 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 336 insertions(+) create mode 100644 ark/jsonschema/object.ts diff --git a/ark/jsonschema/object.ts b/ark/jsonschema/object.ts new file mode 100644 index 0000000000..50de84dc99 --- /dev/null +++ b/ark/jsonschema/object.ts @@ -0,0 +1,336 @@ +import { + ArkErrors, + rootSchema, + type Intersection, + type Predicate, + type TraversalContext +} from "@ark/schema" +import { printable, type show } from "@ark/util" +import type { Type } from "arktype" + +import { innerParseJsonSchema, type inferJsonSchema } from "./json.js" +import { JsonSchema } from "./scope.js" + +const parseMinMaxProperties = ( + jsonSchema: JsonSchema.ObjectSchema, + ctx: TraversalContext +) => { + const predicates: Predicate.Schema[] = [] + if ("maxProperties" in jsonSchema) { + const maxProperties = jsonSchema.maxProperties + + if ((jsonSchema.required?.length ?? 0) > maxProperties) { + ctx.reject({ + message: `The specified JSON Schema requires at least ${jsonSchema.required?.length} properties, which exceeds the specified maxProperties of ${jsonSchema.maxProperties}.` + }) + } + predicates.push((data: object, ctx) => { + const keys = Object.keys(data) + return keys.length <= maxProperties ? + true + : ctx.reject({ + expected: `at most ${maxProperties} propert${maxProperties === 1 ? "y" : "ies"}`, + actual: keys.length.toString() + }) + }) + } + if ("minProperties" in jsonSchema) { + const minProperties = jsonSchema.minProperties + predicates.push((data: object, ctx) => { + const keys = Object.keys(data) + return keys.length >= minProperties ? + true + : ctx.reject({ + expected: `at least ${minProperties} propert${minProperties === 1 ? "y" : "ies"}`, + actual: keys.length.toString() + }) + }) + } + return predicates +} + +const parsePatternProperties = ( + jsonSchema: JsonSchema.ObjectSchema, + ctx: TraversalContext +) => { + if (!("patternProperties" in jsonSchema)) return + + const patternProperties = Object.entries(jsonSchema.patternProperties).map( + ([key, value]) => + [new RegExp(key), innerParseJsonSchema.assert(value)] as const + ) + + // Ensure that the schema for any property is compatible with any corresponding patternProperties + patternProperties.forEach(([pattern, parsedPatternPropertySchema]) => { + Object.entries(jsonSchema.properties ?? {}).forEach( + ([property, schemaForProperty]) => { + if (!pattern.test(property)) return + + const parsedPropertySchema = + innerParseJsonSchema.assert(schemaForProperty) + + if (!parsedPropertySchema.overlaps(parsedPatternPropertySchema)) { + ctx.reject({ + message: `property ${property} must have a schema that overlaps with the patternProperty ${pattern}` + }) + } + } + ) + }) + + // NB: We don't validate compatability of schemas for overlapping patternProperties + // since getting the intersection of regexes is inherenetly difficult. + return (data: object, ctx: TraversalContext) => { + const errors: false[] = [] + + Object.entries(data).forEach(([dataKey, dataValue]) => { + patternProperties.forEach(([pattern, parsedJsonSchema]) => { + if (pattern.test(dataKey) && !parsedJsonSchema.allows(dataValue)) { + errors.push( + ctx.reject({ + actual: dataValue, + expected: `${parsedJsonSchema.description} as property ${dataKey} matches patternProperty ${pattern}` + }) + ) + } + }) + }) + return errors.length === 0 + } +} + +const parsePropertyNames = ( + jsonSchema: JsonSchema.ObjectSchema, + ctx: TraversalContext +) => { + if (!("propertyNames" in jsonSchema)) return + + const propertyNamesValidator = innerParseJsonSchema.assert( + jsonSchema.propertyNames + ) + + if ( + "domain" in propertyNamesValidator.json && + propertyNamesValidator.json.domain !== "string" + ) { + ctx.reject({ + path: ["propertyNames"], + actual: `a schema for validating a ${propertyNamesValidator.json.domain as string}`, + expected: "a schema for validating a string" + }) + } + + return (data: object, ctx: TraversalContext) => { + const errors: false[] = [] + + Object.keys(data).forEach(key => { + if (!propertyNamesValidator.allows(key)) { + errors.push( + ctx.reject({ + message: `property ${key} doesn't adhere to the propertyNames schema of ${propertyNamesValidator.description}` + }) + ) + } + }) + return errors.length === 0 + } +} + +const parseRequiredAndOptionalKeys = ( + jsonSchema: JsonSchema.ObjectSchema, + ctx: TraversalContext +) => { + const optionalKeys: string[] = [] + const requiredKeys: string[] = [] + if ("properties" in jsonSchema) { + if ("required" in jsonSchema) { + if (jsonSchema.required.length !== new Set(jsonSchema.required).size) { + ctx.reject({ + expected: "an array of unique strings", + path: ["required"] + }) + } + + for (const key of jsonSchema.required) { + if (key in jsonSchema.properties) requiredKeys.push(key) + else { + ctx.reject({ + actual: key, + expected: "a key in the 'properties' object", + path: ["required"] + }) + } + } + for (const key in jsonSchema.properties) + if (!jsonSchema.required.includes(key)) optionalKeys.push(key) + } else { + // If 'required' is not present, all keys are optional + optionalKeys.push(...Object.keys(jsonSchema.properties)) + } + } else if ("required" in jsonSchema) { + ctx.reject({ + actual: + "an object JSON Schema with 'required' array but no 'properties' object", + expected: "a valid object JSON Schema" + }) + } + + return { + optionalKeys: optionalKeys.map(key => ({ + key, + value: innerParseJsonSchema.assert(jsonSchema.properties![key]).internal + })), + requiredKeys: requiredKeys.map(key => ({ + key, + value: innerParseJsonSchema.assert(jsonSchema.properties![key]).internal + })) + } +} + +const parseAdditionalProperties = (jsonSchema: JsonSchema.ObjectSchema) => { + if (!("additionalProperties" in jsonSchema)) return + + const properties = Object.keys(jsonSchema.properties ?? {}) + const patternProperties = Object.keys(jsonSchema.patternProperties ?? {}) + + const additionalPropertiesSchema = jsonSchema.additionalProperties + if (additionalPropertiesSchema === true) return + + return (data: object, ctx: TraversalContext) => { + const errors: false[] = [] + + Object.keys(data).forEach(key => { + if ( + properties.includes(key) || + patternProperties.find(pattern => new RegExp(pattern).test(key)) + ) + // Not an additional property, so don't validate here + return + + if (additionalPropertiesSchema === false) { + errors.push( + ctx.reject({ + message: `property ${key} is an additional property, which the provided schema does not allow` + }) + ) + return + } + + const additionalPropertyValidator = innerParseJsonSchema.assert( + additionalPropertiesSchema + ) + + const value = data[key as keyof typeof data] + if (!additionalPropertyValidator.allows(value)) { + errors.push( + ctx.reject({ + problem: `property ${key} is an additional property so must adhere to additional property schema of ${additionalPropertyValidator.description} (was ${printable(value)})` + }) + ) + } + }) + return errors.length === 0 + } +} + +export const validateJsonSchemaObject = JsonSchema.ObjectSchema.pipe( + (jsonSchema, ctx): Type => { + const arktypeObjectSchema: Intersection.Schema = { + domain: "object" + } + + const { requiredKeys, optionalKeys } = parseRequiredAndOptionalKeys( + jsonSchema, + ctx + ) + arktypeObjectSchema.required = requiredKeys + arktypeObjectSchema.optional = optionalKeys + + const predicates: Predicate.Schema[] = [ + ...parseMinMaxProperties(jsonSchema, ctx), + parsePropertyNames(jsonSchema, ctx), + parsePatternProperties(jsonSchema, ctx), + parseAdditionalProperties(jsonSchema) + ].filter(x => x !== undefined) + + const typeWithoutPredicates = rootSchema(arktypeObjectSchema) + console.log(typeWithoutPredicates.json) + if (predicates.length === 0) return typeWithoutPredicates as never + + return rootSchema({ domain: "object", predicate: predicates }).narrow( + (obj: object, innerCtx) => { + const validationResult = typeWithoutPredicates(obj) + if (validationResult instanceof ArkErrors) { + innerCtx.errors.merge(validationResult) + return false + } + return true + } + ) as never + } +) + +type inferAdditionalProperties = + objectSchema["additionalProperties" & keyof objectSchema] extends ( + JsonSchema.Schema + ) ? + objectSchema["additionalProperties" & keyof objectSchema] extends false ? + // false means no additional properties are allowed, + // which is the default in TypeScript so just return the current type. + unknown + : { + // It's not possible in TS to accurately infer additional properties + // so we use `unknown` to at least allow unspecified properties. + [key: string]: unknown + } + : never // TODO: Throw type error + +type inferRequiredProperties = { + [P in (objectSchema["required" & keyof objectSchema] & + string[])[number]]: P extends ( + keyof objectSchema["properties" & keyof objectSchema] + ) ? + objectSchema["properties" & keyof objectSchema][P] extends ( + JsonSchema.Schema + ) ? + inferJsonSchema + : never // TODO: Throw type error + : never // TODO: Throw type error +} + +type inferOptionalProperties = { + [P in keyof objectSchema["properties" & + keyof objectSchema]]?: objectSchema["properties" & + keyof objectSchema][P] extends JsonSchema.Schema ? + inferJsonSchema + : never // TODO: Throw type error +} + +// NB: We don't infer `patternProperties` or 'patternProperties' since regex index signatures are not supported in TS +export type inferJsonSchemaObject = + "properties" extends keyof objectSchema ? + "required" extends keyof objectSchema ? + inferJsonSchemaObject< + Omit & { + properties: Omit< + // Remove the required keys + objectSchema["properties"], + (objectSchema["required"] & string[])[number] + > + }, + inferRequiredProperties + > + : // 'required' isn't present, so all properties are optional + inferJsonSchemaObject< + Omit, + inferOptionalProperties extends ( + Record + ) ? + T + : T & inferOptionalProperties + > + : "additionalProperties" extends keyof objectSchema ? + show> + : // additionalProperties isn't present in the schema, which JSON Schema explicitly + // states means extra properties are allowed, so update types accordingly. + show From 98e9c03a5d37303aedb802ae8e1cb0d7ff99fb50 Mon Sep 17 00:00:00 2001 From: Izan Robinson Date: Mon, 23 Sep 2024 09:08:05 +0100 Subject: [PATCH 21/61] Add parsing for allOf + anyOf + not + oneOf JSON Schemas --- ark/jsonschema/composition.ts | 95 +++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 ark/jsonschema/composition.ts diff --git a/ark/jsonschema/composition.ts b/ark/jsonschema/composition.ts new file mode 100644 index 0000000000..249f1cfd6c --- /dev/null +++ b/ark/jsonschema/composition.ts @@ -0,0 +1,95 @@ +import type { array } from "@ark/util" +import { type, type Type } from "arktype" +import { innerParseJsonSchema, type inferJsonSchema } from "./json.js" +import type { JsonSchema } from "./scope.js" + +const validateAllOfJsonSchemas = ( + jsonSchemas: JsonSchema.Schema[] +): Type => + jsonSchemas + .map(jsonSchema => innerParseJsonSchema.assert(jsonSchema)) + .reduce((acc, validator) => acc.and(validator)) + +const validateAnyOfJsonSchemas = ( + jsonSchemas: JsonSchema.Schema[] +): Type => + jsonSchemas + .map(jsonSchema => innerParseJsonSchema.assert(jsonSchema)) + .reduce((acc, validator) => acc.or(validator)) + +const validateNotJsonSchema = (jsonSchema: JsonSchema.Schema) => { + const inner = innerParseJsonSchema.assert(jsonSchema) + return type("unknown").narrow((data, ctx) => + inner.allows(data) ? ctx.mustBe(`not ${inner.description}`) : true + ) as Type +} + +const validateOneOfJsonSchemas = (jsonSchemas: JsonSchema.Schema[]) => { + const oneOfValidators = jsonSchemas.map(nestedSchema => + innerParseJsonSchema.assert(nestedSchema) + ) + const oneOfValidatorsDescriptions = oneOfValidators.map( + validator => `○ ${validator.description}` + ) + return ( + ( + type("unknown").narrow((data, ctx) => { + let matchedValidator: Type | undefined = undefined + + for (const validator of oneOfValidators) { + if (validator.allows(data)) { + if (matchedValidator === undefined) { + matchedValidator = validator + continue + } + return ctx.mustBe( + `exactly one of:\n${oneOfValidatorsDescriptions.join("\n")}` + ) + } + } + return matchedValidator !== undefined + }) as Type + ) + // TODO: Theoretically this shouldn't be necessary due to above `ctx.mustBe` in narrow??? + .describe(`one of:\n${oneOfValidatorsDescriptions.join("\n")}\n`) + ) +} + +export const parseJsonSchemaCompositionKeywords = ( + jsonSchema: + | JsonSchema.TypeWithNoKeywords + | JsonSchema.TypeWithKeywords + | JsonSchema.AnyKeywords + | JsonSchema.CompositionKeywords +): Type | undefined => { + if ("allOf" in jsonSchema) return validateAllOfJsonSchemas(jsonSchema.allOf) + if ("anyOf" in jsonSchema) return validateAnyOfJsonSchemas(jsonSchema.anyOf) + if ("not" in jsonSchema) return validateNotJsonSchema(jsonSchema.not) + if ("oneOf" in jsonSchema) return validateOneOfJsonSchemas(jsonSchema.oneOf) +} + +// NB: For simplicity sake, the type level treats 'anyOf' and 'oneOf' as the same. +type inferJsonSchemaAnyOrOneOf = + compositionSchemaValue extends never[] ? + never // is an empty array, so is invalid + : compositionSchemaValue extends array ? + t & inferJsonSchema + : never // is not an array, so is invalid + +export type inferJsonSchemaComposition = + "allOf" extends keyof schema ? + t extends never ? + t // "allOf" has incompatible schemas, so don't keep looking + : schema["allOf"] extends [infer firstSchema, ...infer restOfSchemas] ? + inferJsonSchemaComposition< + { allOf: restOfSchemas }, + inferJsonSchema + > + : schema["allOf"] extends never[] ? + t // have finished inferring schemas + : never // "allOf" isn't an array, so is invalid + : "oneOf" extends keyof schema ? inferJsonSchemaAnyOrOneOf + : "anyOf" extends keyof schema ? inferJsonSchemaAnyOrOneOf + : "not" extends keyof schema ? + t // NB: TypeScript doesn't have "not" types, so can't accurately represent. + : unknown From 6dbdbcd6d54f3fcd2209355cc00aa1a2af7eed44 Mon Sep 17 00:00:00 2001 From: Izan Robinson Date: Mon, 23 Sep 2024 09:08:33 +0100 Subject: [PATCH 22/61] Add parsing for const + enum JSON Schemas --- ark/jsonschema/any.ts | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 ark/jsonschema/any.ts diff --git a/ark/jsonschema/any.ts b/ark/jsonschema/any.ts new file mode 100644 index 0000000000..975d3b5c9d --- /dev/null +++ b/ark/jsonschema/any.ts @@ -0,0 +1,29 @@ +import { rootSchema } from "@ark/schema" +import { throwParseError } from "@ark/util" +import type { Type } from "arktype" +import type { JsonSchema } from "./scope.ts" + +export const parseJsonSchemaAnyKeywords = ( + jsonSchema: + | JsonSchema.TypeWithNoKeywords + | JsonSchema.TypeWithKeywords + | JsonSchema.AnyKeywords + | JsonSchema.CompositionKeywords +): Type | undefined => { + if ("const" in jsonSchema) { + if ("enum" in jsonSchema) { + throwParseError( + "Provided JSON Schema cannot have both 'const' and 'enum' keywords." + ) + } + return rootSchema({ unit: jsonSchema.const }) as unknown as Type + } + + if ("enum" in jsonSchema) { + return rootSchema( + jsonSchema.enum.map((unit: unknown) => ({ + unit + })) + ) as unknown as Type + } +} From 7248b278c86c47d3df095e7898b6bb17f24113e9 Mon Sep 17 00:00:00 2001 From: Izan Robinson Date: Mon, 23 Sep 2024 09:09:13 +0100 Subject: [PATCH 23/61] Add core parseJsonSchema with parsing and type inference logic --- ark/jsonschema/json.ts | 133 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 ark/jsonschema/json.ts diff --git a/ark/jsonschema/json.ts b/ark/jsonschema/json.ts new file mode 100644 index 0000000000..685eed5435 --- /dev/null +++ b/ark/jsonschema/json.ts @@ -0,0 +1,133 @@ +import { + printable, + throwParseError, + type array, + type ErrorMessage +} from "@ark/util" +import { type, type Out, type Type } from "arktype" +import { parseJsonSchemaAnyKeywords } from "./any.ts" +import { validateJsonSchemaArray, type inferJsonSchemaArray } from "./array.js" +import { + parseJsonSchemaCompositionKeywords, + type inferJsonSchemaComposition +} from "./composition.ts" +import { + validateJsonSchemaNumber, + type inferJsonSchemaNumber +} from "./number.js" +import { + validateJsonSchemaObject, + type inferJsonSchemaObject +} from "./object.js" +import { JsonSchema } from "./scope.js" +import { + validateJsonSchemaString, + type inferJsonSchemaString +} from "./string.js" + +type JsonSchemaConstraintKind = "const" | "enum" +type JsonSchemaConst = { const: t } +type JsonSchemaEnum = { enum: readonly t[] } + +type inferJsonSchemaConstraint< + schema, + t, + kind extends JsonSchemaConstraintKind +> = t extends never ? never : t & inferJsonSchema> + +type inferJsonSchemaTypeNoKeywords< + schema extends JsonSchema.TypeWithNoKeywords, + t +> = + schema["type"] extends "boolean" ? t & boolean + : schema["type"] extends "null" ? t & null + : never + +export type inferJsonSchema = + schema extends true ? JsonSchema.Json + : schema extends false ? never + : schema extends Record ? JsonSchema.Json + : schema extends array ? inferJsonSchema + : schema extends JsonSchema.CompositionKeywords ? + inferJsonSchemaComposition + : schema extends JsonSchemaConst ? + inferJsonSchemaConstraint + : schema extends JsonSchemaEnum ? + inferJsonSchemaConstraint + : schema extends JsonSchema.TypeWithNoKeywords ? + inferJsonSchemaTypeNoKeywords + : schema extends JsonSchema.ArraySchema ? inferJsonSchemaArray + : schema extends JsonSchema.NumberSchema ? t & inferJsonSchemaNumber + : schema extends JsonSchema.ObjectSchema ? t & inferJsonSchemaObject + : schema extends JsonSchema.StringSchema ? t & inferJsonSchemaString + : t extends {} ? t + : ErrorMessage<"Failed to infer JSON Schema"> + +export const innerParseJsonSchema: Type< + (In: JsonSchema.Schema) => Out> +> = JsonSchema.Schema.pipe( + (jsonSchema: JsonSchema.Schema): Type => { + if (typeof jsonSchema === "boolean") { + if (jsonSchema) return JsonSchema.Json + else return type("never") // No runtime value ever passes validation for JSON schema of 'false' + } + + if (Array.isArray(jsonSchema)) { + return ( + parseJsonSchemaCompositionKeywords({ anyOf: jsonSchema }) ?? + throwParseError( + "Failed to convert root array of JSON Schemas to an anyOf schema" + ) + ) + } + + const constAndOrEnumValidator = parseJsonSchemaAnyKeywords(jsonSchema) + const compositionValidator = parseJsonSchemaCompositionKeywords(jsonSchema) + + const preTypeValidator: Type | undefined = + constAndOrEnumValidator ? + compositionValidator ? compositionValidator.and(constAndOrEnumValidator) + : constAndOrEnumValidator + : compositionValidator + + if ("type" in jsonSchema) { + let typeValidator: Type + switch (jsonSchema.type) { + case "array": + typeValidator = validateJsonSchemaArray.assert(jsonSchema) + break + case "boolean": + case "null": + typeValidator = type(jsonSchema.type) + break + case "integer": + case "number": + typeValidator = validateJsonSchemaNumber.assert(jsonSchema) + break + case "object": + typeValidator = validateJsonSchemaObject.assert(jsonSchema) + break + case "string": + typeValidator = validateJsonSchemaString.assert(jsonSchema) + break + default: + throwParseError( + // @ts-expect-error -- All valid 'type' values should be handled above + `Provided 'type' value must be a supported JSON Schema type (was '${jsonSchema.type}')` + ) + } + if (preTypeValidator === undefined) return typeValidator + return typeValidator.and(preTypeValidator) + } + if (preTypeValidator === undefined) { + throwParseError( + `Provided JSON Schema must have one of 'type', 'enum', 'const', 'allOf', 'anyOf' but was ${printable(jsonSchema)}.` + ) + } + return preTypeValidator // TODO: Is this actually the correct thing to return??? + } +) + +export const parseJsonSchema = ( + jsonSchema: t +): Type> => innerParseJsonSchema.assert(jsonSchema) as never From 79c0a79c18dc2af4ed56467b7c824cea05d1d856 Mon Sep 17 00:00:00 2001 From: Izan Robinson Date: Mon, 23 Sep 2024 09:09:57 +0100 Subject: [PATCH 24/61] Add ark/jsonschema/index.ts entry level to @ark/jsonschema --- ark/jsonschema/index.ts | 2 ++ ark/jsonschema/package.json | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 ark/jsonschema/index.ts diff --git a/ark/jsonschema/index.ts b/ark/jsonschema/index.ts new file mode 100644 index 0000000000..0b23b9ea49 --- /dev/null +++ b/ark/jsonschema/index.ts @@ -0,0 +1,2 @@ +export { parseJsonSchema } from "./json.js" +export * from "./scope.js" diff --git a/ark/jsonschema/package.json b/ark/jsonschema/package.json index cc944ff40d..23c188fd0a 100644 --- a/ark/jsonschema/package.json +++ b/ark/jsonschema/package.json @@ -12,10 +12,10 @@ "url": "https://github.com/arktypeio/arktype.git" }, "type": "module", - "main": "./out/api.js", - "types": "./out/api.d.ts", + "main": "./out/index.js", + "types": "./out/index.d.ts", "exports": { - ".": "./out/api.js", + ".": "./out/index.js", "./internal/*": "./out/*" }, "files": [ From 28da503057f9cc88f00df968fe4ce628f04751f9 Mon Sep 17 00:00:00 2001 From: Izan Robinson Date: Mon, 23 Sep 2024 09:10:56 +0100 Subject: [PATCH 25/61] Add @ark/jsonschema to root package.json devDepencies --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 58b75ac36f..5c309f7228 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "@ark/attest-ts-min": "catalog:", "@ark/attest-ts-next": "catalog:", "@ark/fs": "workspace:*", + "@ark/jsonschema": "workspace:*", "@ark/repo": "workspace:*", "@ark/util": "workspace:*", "@eslint/js": "9.10.0", From 474a84996e69f86b08d5d4976c4a49a1d410aabe Mon Sep 17 00:00:00 2001 From: Izan Robinson Date: Mon, 23 Sep 2024 09:29:58 +0100 Subject: [PATCH 26/61] Export type Out from ark/type/keywords/inference.ts --- ark/type/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ark/type/index.ts b/ark/type/index.ts index 0546ec567d..9bcccc2602 100644 --- a/ark/type/index.ts +++ b/ark/type/index.ts @@ -7,6 +7,7 @@ export { } from "@ark/schema" export { Hkt, inferred } from "@ark/util" export { Generic } from "./generic.ts" +export type { Out } from "./keywords/inference.ts" export { ark, declare, @@ -19,3 +20,4 @@ export { export { Module, type BoundModule, type Submodule } from "./module.ts" export { module, scope, type Scope } from "./scope.ts" export { Type } from "./type.ts" + From 95e1b40a1356be2a38ea7b639591c4e42869106e Mon Sep 17 00:00:00 2001 From: Izan Robinson Date: Mon, 23 Sep 2024 09:44:44 +0100 Subject: [PATCH 27/61] Fix @ark/jsonschema use of arktype constraint inference --- ark/jsonschema/array.ts | 12 +++--------- ark/type/index.ts | 7 ++++++- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/ark/jsonschema/array.ts b/ark/jsonschema/array.ts index 36aa05dd86..ce6a371727 100644 --- a/ark/jsonschema/array.ts +++ b/ark/jsonschema/array.ts @@ -5,7 +5,7 @@ import { type TraversalContext } from "@ark/schema" import { printable, type array } from "@ark/util" -import type { Type, applyConstraint, schemaToConstraint } from "arktype" +import type { Type, applyConstraintSchema } from "arktype" import { innerParseJsonSchema, type inferJsonSchema } from "./json.js" import { JsonSchema } from "./scope.js" @@ -136,18 +136,12 @@ type inferJsonSchemaArrayConstraints = "maxItems" extends keyof arraySchema ? inferJsonSchemaArrayConstraints< Omit, - applyConstraint< - T, - schemaToConstraint<"maxLength", arraySchema["maxItems"] & number> - > + applyConstraintSchema > : "minItems" extends keyof arraySchema ? inferJsonSchemaArrayConstraints< Omit, - applyConstraint< - T, - schemaToConstraint<"minLength", arraySchema["minItems"] & number> - > + applyConstraintSchema > : T extends {} ? T : never diff --git a/ark/type/index.ts b/ark/type/index.ts index 9bcccc2602..c6dd8fa0d8 100644 --- a/ark/type/index.ts +++ b/ark/type/index.ts @@ -7,7 +7,12 @@ export { } from "@ark/schema" export { Hkt, inferred } from "@ark/util" export { Generic } from "./generic.ts" -export type { Out } from "./keywords/inference.ts" +export type { + Out, + applyConstraintSchema, + number, + string +} from "./keywords/inference.ts" export { ark, declare, From 2e2155398982b97462f06af20c7f3a2077ac500b Mon Sep 17 00:00:00 2001 From: Izan Robinson Date: Wed, 2 Oct 2024 16:25:21 +0100 Subject: [PATCH 28/61] Preliminary tests for @ark/jsonschema --- ark/jsonschema/__tests__/array.test.ts | 130 ++++++++++++++++++++++++ ark/jsonschema/__tests__/json.test.ts | 88 ++++++++++++++++ ark/jsonschema/__tests__/number.test.ts | 97 ++++++++++++++++++ ark/jsonschema/__tests__/object.test.ts | 109 ++++++++++++++++++++ ark/jsonschema/__tests__/string.test.ts | 63 ++++++++++++ 5 files changed, 487 insertions(+) create mode 100644 ark/jsonschema/__tests__/array.test.ts create mode 100644 ark/jsonschema/__tests__/json.test.ts create mode 100644 ark/jsonschema/__tests__/number.test.ts create mode 100644 ark/jsonschema/__tests__/object.test.ts create mode 100644 ark/jsonschema/__tests__/string.test.ts diff --git a/ark/jsonschema/__tests__/array.test.ts b/ark/jsonschema/__tests__/array.test.ts new file mode 100644 index 0000000000..09d8f6d800 --- /dev/null +++ b/ark/jsonschema/__tests__/array.test.ts @@ -0,0 +1,130 @@ +import { attest, contextualize } from "@ark/attest" +import { parseJsonSchema } from "@ark/jsonschema" + +// TODO: Add compound tests for arrays (e.g. maxItems AND minItems ) +// TODO: Add explicit test for negative length constraint failing (since explicitly mentioned in spec) + +contextualize(() => { + it("type array", () => { + const t = parseJsonSchema({ type: "array" }) + attest(t.infer) + attest(t.json).snap({ proto: "Array" }) + }) + + it("items & additionalItems", () => { + const tItems = parseJsonSchema({ + type: "array", + items: [{ type: "string" }, { type: "number" }] + }) + attest<[string, number]>(tItems.infer) + attest(tItems.json).snap({ + proto: "Array", + sequence: { prefix: ["string", "number"] }, + exactLength: 2 + }) + attest(tItems.allows(["foo", 1])).equals(true) + attest(tItems.allows([1, "foo"])).equals(false) + attest(tItems.allows(["foo", 1, true])).equals(false) + + const tItemsVariadic = parseJsonSchema({ + type: "array", + items: [{ type: "string" }, { type: "number" }], + additionalItems: { type: "boolean" } + }) + attest<[string, number, ...boolean[]]>(tItemsVariadic.infer) + attest(tItemsVariadic.json).snap({ + minLength: 2, + proto: "Array", + sequence: { + prefix: ["string", "number"], + variadic: [{ unit: false }, { unit: true }] + } + }) + attest(tItemsVariadic.allows(["foo", 1])).equals(true) + attest(tItemsVariadic.allows([1, "foo", true])).equals(false) + attest(tItemsVariadic.allows([false, "foo", 1])).equals(false) + attest(tItemsVariadic.allows(["foo", 1, true])).equals(true) + }) + + it("contains", () => { + const tContains = parseJsonSchema({ + type: "array", + contains: { type: "number" } + }) + const predicateRef = + tContains.internal.firstReferenceOfKindOrThrow( + "predicate" + ).serializedPredicate + attest(tContains.infer) + attest(tContains.json).snap({ + proto: "Array", + predicate: [predicateRef] + }) + attest(tContains.allows([])).equals(false) + attest(tContains.allows([1, 2, 3])).equals(true) + attest(tContains.allows(["foo", "bar", "baz"])).equals(false) + }) + + it("maxItems", () => { + const tMaxItems = parseJsonSchema({ + type: "array", + maxItems: 5 + }) + attest(tMaxItems.infer) + attest(tMaxItems.json).snap({ + proto: "Array", + maxLength: 5 + }) + + attest(() => parseJsonSchema({ type: "array", maxItems: -1 })).throws( + "maxItems must be an integer >= 0" + ) + }) + + it("minItems", () => { + const tMinItems = parseJsonSchema({ + type: "array", + minItems: 5 + }) + attest(tMinItems.infer) + attest(tMinItems.json).snap({ + proto: "Array", + minLength: 5 + }) + + attest(() => parseJsonSchema({ type: "array", minItems: -1 })).throws( + "minItems must be an integer >= 0" + ) + }) + + it("uniqueItems", () => { + const tUniqueItems = parseJsonSchema({ + type: "array", + uniqueItems: true + }) + const predicateRef = + tUniqueItems.internal.firstReferenceOfKindOrThrow( + "predicate" + ).serializedPredicate + attest(tUniqueItems.infer) + attest(tUniqueItems.json).snap({ + proto: "Array", + predicate: [predicateRef] + }) + attest(tUniqueItems.allows([1, 2, 3])).equals(true) + attest(tUniqueItems.allows([1, 1, 2])).equals(false) + attest( + tUniqueItems.allows([ + { foo: { bar: ["baz", { qux: "quux" }] } }, + { foo: { bar: ["baz", { qux: "quux" }] } } + ]) + ).equals(false) + attest( + // JSON Schema specifies that arrays must be same order to be classified as equal + tUniqueItems.allows([ + { foo: { bar: ["baz", { qux: "quux" }] } }, + { foo: { bar: [{ qux: "quux" }, "baz"] } } + ]) + ).equals(true) + }) +}) diff --git a/ark/jsonschema/__tests__/json.test.ts b/ark/jsonschema/__tests__/json.test.ts new file mode 100644 index 0000000000..cd92508606 --- /dev/null +++ b/ark/jsonschema/__tests__/json.test.ts @@ -0,0 +1,88 @@ +import { attest, contextualize } from "@ark/attest" +import { parseJsonSchema } from "@ark/jsonschema" +import type { applyConstraintSchema, number } from "arktype" + +contextualize(() => { + it("array", () => { + // unknown[] + const parsedJsonSchemaArray = parseJsonSchema({ type: "array" } as const) + attest(parsedJsonSchemaArray.infer) + attest(parsedJsonSchemaArray.json).snap({ proto: "Array" }) + + // number[] + const parsedJsonSchemaArrayVariadic = parseJsonSchema({ + type: "array", + items: { type: "number", minimum: 3 } + } as const) + attest(parsedJsonSchemaArrayVariadic.infer) + attest(parsedJsonSchemaArrayVariadic.json).snap({ + proto: "Array", + sequence: { domain: "number", min: 3 } + }) + attest[]>(parsedJsonSchemaArrayVariadic.inferBrandableOut) + + // [string] + const parsedJsonSchemaArrayFixed = parseJsonSchema({ + type: "array", + items: [{ type: "string" }] + } as const) + attest<[string]>(parsedJsonSchemaArrayFixed.infer) + attest(parsedJsonSchemaArrayFixed.json).snap({ + exactLength: 1, + proto: "Array", + sequence: { prefix: ["string"] } + }) + + // // [string, ...number[]] + const parsedJsonSchemaArrayFixedWithVariadic = parseJsonSchema({ + type: "array", + items: [{ type: "string" }], + additionalItems: { type: "number" } + } as const) + attest<[string, ...number[]]>(parsedJsonSchemaArrayFixedWithVariadic.infer) + + // Maximum Length + const parsedJsonSchemaArrayMaxLength = parseJsonSchema({ + type: "array", + items: { type: "string" }, + maxItems: 5 + } as const) + attest(parsedJsonSchemaArrayMaxLength.infer) + attest>( + parsedJsonSchemaArrayMaxLength.tOut + ) + + // Minimum Length + const parsedJsonSchemaArrayMinLength = parseJsonSchema({ + type: "array", + items: { type: "number" }, + minItems: 3 + } as const) + attest(parsedJsonSchemaArrayMinLength.infer) + attest>( + parsedJsonSchemaArrayMinLength.tOut + ) + + // Maximum & Minimum Length + const parsedJsonSchemaArrayMaxAndMinLength = parseJsonSchema({ + type: "array", + items: { type: "array", items: { type: "string" } }, + maxItems: 5, + minItems: 3 + } as const) + attest(parsedJsonSchemaArrayMaxAndMinLength.infer) + attest< + applyConstraintSchema< + applyConstraintSchema, + "minLength", + 3 + > + >(parsedJsonSchemaArrayMaxAndMinLength.tOut) + }) + + it("number", () => {}) + + it("object", () => {}) + + it("string", () => {}) +}) diff --git a/ark/jsonschema/__tests__/number.test.ts b/ark/jsonschema/__tests__/number.test.ts new file mode 100644 index 0000000000..90ac504e56 --- /dev/null +++ b/ark/jsonschema/__tests__/number.test.ts @@ -0,0 +1,97 @@ +import { attest, contextualize } from "@ark/attest" +import { parseJsonSchema } from "@ark/jsonschema" + +// TODO: Compound tests for number (e.g. 'minimum' AND 'maximum') + +contextualize(() => { + it("type number", () => { + const jsonSchema = { type: "number" } as const + const expectedArkTypeSchema = { domain: "number" } as const + + const parsedNumberValidator = parseJsonSchema(jsonSchema) + attest(parsedNumberValidator.infer) + attest(parsedNumberValidator.json).snap(expectedArkTypeSchema) + }) + + it("type integer", () => { + const t = parseJsonSchema({ type: "integer" }) + attest(t.infer) + attest(t.json).snap({ domain: "number", divisor: 1 }) + }) + + it("maximum & exclusiveMaximum", () => { + const tMax = parseJsonSchema({ + type: "number", + maximum: 5 + }) + attest(tMax.infer) + attest(tMax.json).snap({ + domain: "number", + max: 5 + }) + + const tExclMax = parseJsonSchema({ + type: "number", + exclusiveMaximum: 5 + }) + attest(tExclMax.infer) + attest(tExclMax.json).snap({ + domain: "number", + max: { rule: 5, exclusive: true } + }) + + attest(() => + parseJsonSchema({ + type: "number", + maximum: 5, + exclusiveMaximum: 5 + }) + ).throws( + "ParseError: Provided number JSON Schema cannot have 'maximum' and 'exclusiveMaximum" + ) + }) + + it("minimum & exclusiveMinimum", () => { + const tMin = parseJsonSchema({ type: "number", minimum: 5 }) + attest(tMin.infer) + attest(tMin.json).snap({ domain: "number", min: 5 }) + + const tExclMin = parseJsonSchema({ + type: "number", + exclusiveMinimum: 5 + }) + attest(tExclMin.infer) + attest(tExclMin.json).snap({ + domain: "number", + min: { rule: 5, exclusive: true } + }) + + attest(() => + parseJsonSchema({ + type: "number", + minimum: 5, + exclusiveMinimum: 5 + }) + ).throws( + "ParseError: Provided number JSON Schema cannot have 'minimum' and 'exclusiveMinimum" + ) + }) + + it("multipleOf", () => { + const t = parseJsonSchema({ type: "number", multipleOf: 5 }) + attest(t.infer) + attest(t.json).snap({ domain: "number", divisor: 5 }) + + const tInt = parseJsonSchema({ + type: "integer", + multipleOf: 5 + }) + attest(tInt.infer) + attest(tInt.json).snap({ domain: "number", divisor: 5 }) + + // JSON Schema allows decimal multipleOf, but ArkType doesn't. + attest(() => parseJsonSchema({ type: "number", multipleOf: 5.5 })).throws( + "AggregateError: multipleOf must be an integer" + ) + }) +}) diff --git a/ark/jsonschema/__tests__/object.test.ts b/ark/jsonschema/__tests__/object.test.ts new file mode 100644 index 0000000000..00c155e719 --- /dev/null +++ b/ark/jsonschema/__tests__/object.test.ts @@ -0,0 +1,109 @@ +import { attest, contextualize } from "@ark/attest" +import { parseJsonSchema } from "@ark/jsonschema" + +// TODO: Add compound tests for objects (e.g. 'maxProperties' AND 'minProperties') + +contextualize(() => { + it("type object", () => { + const t = parseJsonSchema({ type: "object" }) + attest<{ [x: string]: unknown }>(t.infer) + attest(t.json).snap({ domain: "object" }) + }) + + it("maxProperties", () => { + const tMaxProperties = parseJsonSchema({ + type: "object", + maxProperties: 1 + }) + attest(tMaxProperties.infer) + attest(tMaxProperties.json).snap({ domain: "object" }) + attest(tMaxProperties.allows({})).equals(true) + attest(tMaxProperties.allows({ foo: 1 })).equals(true) + attest(tMaxProperties.allows({ foo: 1, bar: 2 })).equals(false) + attest(tMaxProperties.allows({ foo: 1, bar: 2, baz: 3 })).equals(false) + }) + + it("minProperties", () => { + const tMinProperties = parseJsonSchema({ + type: "object", + minProperties: 2 + }) + attest(tMinProperties.infer) + attest(tMinProperties.json).snap({ domain: "object" }) + attest(tMinProperties.allows({})).equals(false) + attest(tMinProperties.allows({ foo: 1 })).equals(false) + attest(tMinProperties.allows({ foo: 1, bar: 2 })).equals(true) + attest(tMinProperties.allows({ foo: 1, bar: 2, baz: 3 })).equals(true) + }) + + it("properties & required", () => { + const tRequired = parseJsonSchema({ + type: "object", + properties: { + foo: { type: "string" }, + bar: { type: "number" } + }, + required: ["foo"] + }) + attest<{ [x: string]: unknown; foo: string; bar?: number }>(tRequired.infer) + attest(tRequired.json).snap({ + domain: "object", + required: [{ key: "foo", value: "string" }], + optional: [{ key: "bar", value: "number" }] + }) + + attest(() => parseJsonSchema({ type: "object", required: ["foo"] })).throws( + "'required' array is present but 'properties' object is missing" + ) + attest(() => + parseJsonSchema({ + type: "object", + properties: { foo: { type: "string" } }, + required: ["bar"] + }) + ).throws( + "Key 'bar' in 'required' array is not present in 'properties' object" + ) + attest(() => + parseJsonSchema({ + type: "object", + properties: { foo: { type: "string" } }, + required: ["foo", "foo"] + }) + ).throws("Duplicate keys in 'required' array") + }) + + it("additionalProperties", () => { + const tAdditionalProperties = parseJsonSchema({ + type: "object", + additionalProperties: { type: "number" } + }) + attest<{ [x: string]: unknown }>(tAdditionalProperties.infer) + attest(tAdditionalProperties.json).snap({ + domain: "object", + additional: "number" + }) + attest(tAdditionalProperties.allows({})).equals(true) + attest(tAdditionalProperties.allows({ foo: 1 })).equals(true) + attest(tAdditionalProperties.allows({ foo: 1, bar: 2 })).equals(true) + attest(tAdditionalProperties.allows({ foo: 1, bar: "2" })).equals(false) + }) + + it("patternProperties", () => { + const tPatternProperties = parseJsonSchema({ + type: "object", + patternProperties: { + "^[a-z]+$": { type: "string" } + } + }) + attest<{ [x: string]: unknown }>(tPatternProperties.infer) + attest(tPatternProperties.json).snap({ + domain: "object", + pattern: [{ key: "^[a-z]+$", value: "string" }] + }) + attest(tPatternProperties.allows({})).equals(true) + attest(tPatternProperties.allows({ foo: "bar" })).equals(true) + attest(tPatternProperties.allows({ foo: 1 })).equals(false) + attest(tPatternProperties.allows({ "123": "bar" })).equals(false) + }) +}) diff --git a/ark/jsonschema/__tests__/string.test.ts b/ark/jsonschema/__tests__/string.test.ts new file mode 100644 index 0000000000..a28ebedfb1 --- /dev/null +++ b/ark/jsonschema/__tests__/string.test.ts @@ -0,0 +1,63 @@ +import { attest, contextualize } from "@ark/attest" +import { parseJsonSchema } from "@ark/jsonschema" + +// TODO: Add compound tests for strings (e.g. maxLength AND pattern) +// TODO: Add explicit test for negative length constraint failing (since explicitly mentioned in spec) + +contextualize(() => { + it("type string", () => { + const t = parseJsonSchema({ type: "string" }) + attest(t.infer) + attest(t.json).snap({ domain: "string" }) + }) + + it("maxLength", () => { + const tMaxLength = parseJsonSchema({ + type: "string", + maxLength: 5 + }) + attest(tMaxLength.infer) + attest(tMaxLength.json).snap({ + domain: "string", + maxLength: 5 + }) + }) + + it("minLength", () => { + const tMinLength = parseJsonSchema({ + type: "string", + minLength: 5 + }) + attest(tMinLength.infer) + attest(tMinLength.json).snap({ + domain: "string", + minLength: 5 + }) + }) + + it("pattern", () => { + const tPatternString = parseJsonSchema({ + type: "string", + pattern: "es" + }) + attest(tPatternString.infer) + attest(tPatternString.json).snap({ + domain: "string", + regex: ["es"] + }) + // JSON Schema explicitly specifies that regexes MUST NOT be implicitly anchored + // https://json-schema.org/draft-07/draft-handrews-json-schema-validation-01#rfc.section.4.3 + attest(tPatternString.allows("expression")).equals(true) + + const tPatternRegExp = parseJsonSchema({ + type: "string", + pattern: /es/ + }) + attest(tPatternRegExp.infer) + attest(tPatternRegExp.json).snap({ + domain: "string", + regex: ["es"] // strips the outer slashes + }) + attest(tPatternRegExp.allows("expression")).equals(true) + }) +}) From 6903094ec24e29f2655ac264cd364ddb960f8bca Mon Sep 17 00:00:00 2001 From: TizzySaurus Date: Wed, 2 Oct 2024 20:59:43 +0100 Subject: [PATCH 29/61] Add ts-ignore comment for excessively deep tuple spread --- ark/jsonschema/array.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/ark/jsonschema/array.ts b/ark/jsonschema/array.ts index ce6a371727..23c9bf93f7 100644 --- a/ark/jsonschema/array.ts +++ b/ark/jsonschema/array.ts @@ -117,6 +117,7 @@ export type inferJsonSchemaArray = arraySchema["items"] extends array ? inferJsonSchemaArrayConstraints< Omit, + // @ts-ignore - TypeScript complains that this is "excessively deep", despite it correctly resolving the type [ ...inferJsonSchemaArrayItems, ...inferJsonSchema[] From d65fa30c18f6a0aae65c571d8138ecaffa56e766 Mon Sep 17 00:00:00 2001 From: TizzySaurus Date: Wed, 2 Oct 2024 21:19:55 +0100 Subject: [PATCH 30/61] Add preliminary README.md for @ark/jsonschema --- ark/jsonschema/README.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/ark/jsonschema/README.md b/ark/jsonschema/README.md index 8d041f79a9..42fc2a5cc1 100644 --- a/ark/jsonschema/README.md +++ b/ark/jsonschema/README.md @@ -1 +1,33 @@ # @arktype/jsonschema + +## What is it? +@arktype/jsonschema is a package that allows converting from a JSON Schema schema, to an ArkType type. For example: +```js +import { parseJsonSchema } from "@ark/jsonschema" + +const t = parseJsonSchema({type: "string", minLength: 5, maxLength: 10}) +``` +is equivalent to: +```js +import { type } from "arktype" + +const t = type("5<=string<=10") +``` +This enables easy adoption of ArkType for people who currently have JSON Schema based runtime validation in their codebase. + +Where possible, the library also has TypeScript type inference so that the runtime validation remains typesafe. Extending on the above example, this means that the return type of the below `parseString` function would be correctly inferred as `string`: +```ts +const assertIsString = (data: unknown) + return t.assert(data) +``` + +## Extra Type Safety +If you wish to ensure that your JSON Schema schemas are valid, you can do this too! Simply import the relevant schema type from `@ark/jsonschema`, like below: +```ts +import type { JsonSchemaString } from "@ark/jsonschema" + +const schema: JsonSchemaString = { + type: "string", + minLength: "3" // errors stating that 'minLength' must be a number +} +``` \ No newline at end of file From 8e97d872807e06c6f6699c10fddd60b4fa2e8969 Mon Sep 17 00:00:00 2001 From: TizzySaurus Date: Wed, 2 Oct 2024 21:20:11 +0100 Subject: [PATCH 31/61] Linting --- ark/jsonschema/CHANGELOG.md | 3 ++- ark/type/index.ts | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ark/jsonschema/CHANGELOG.md b/ark/jsonschema/CHANGELOG.md index a8a11d4cff..4f4fb7479e 100644 --- a/ark/jsonschema/CHANGELOG.md +++ b/ark/jsonschema/CHANGELOG.md @@ -7,6 +7,7 @@ Released the initial implementation of the package. Known limitations: + - No `dependencies` support - No `if`/`else`/`then` support -- `multipleOf` only supports integers \ No newline at end of file +- `multipleOf` only supports integers diff --git a/ark/type/index.ts b/ark/type/index.ts index c6dd8fa0d8..5b91c86d1f 100644 --- a/ark/type/index.ts +++ b/ark/type/index.ts @@ -25,4 +25,3 @@ export { export { Module, type BoundModule, type Submodule } from "./module.ts" export { module, scope, type Scope } from "./scope.ts" export { Type } from "./type.ts" - From 877f36ea3d080f217f8a0c17cb717b6812f93850 Mon Sep 17 00:00:00 2001 From: TizzySaurus Date: Wed, 2 Oct 2024 21:22:39 +0100 Subject: [PATCH 32/61] Fix example in README.md --- ark/jsonschema/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ark/jsonschema/README.md b/ark/jsonschema/README.md index 42fc2a5cc1..24eef57370 100644 --- a/ark/jsonschema/README.md +++ b/ark/jsonschema/README.md @@ -22,11 +22,11 @@ const assertIsString = (data: unknown) ``` ## Extra Type Safety -If you wish to ensure that your JSON Schema schemas are valid, you can do this too! Simply import the relevant schema type from `@ark/jsonschema`, like below: +If you wish to ensure that your JSON Schema schemas are valid, you can do this too! Simply import the `JsonSchema` namespace type from `@ark/jsonschema`, and use the appropriate member like so: ```ts -import type { JsonSchemaString } from "@ark/jsonschema" +import type { JsonSchema } from "@ark/jsonschema" -const schema: JsonSchemaString = { +const schema: JsonSchema.StringSchema = { type: "string", minLength: "3" // errors stating that 'minLength' must be a number } From d59336c300e338e704f2774d4f44e0f2664c708a Mon Sep 17 00:00:00 2001 From: TizzySaurus Date: Wed, 2 Oct 2024 21:23:11 +0100 Subject: [PATCH 33/61] Remove extra double-slash in comment --- ark/jsonschema/__tests__/json.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ark/jsonschema/__tests__/json.test.ts b/ark/jsonschema/__tests__/json.test.ts index cd92508606..fd54c1170a 100644 --- a/ark/jsonschema/__tests__/json.test.ts +++ b/ark/jsonschema/__tests__/json.test.ts @@ -33,7 +33,7 @@ contextualize(() => { sequence: { prefix: ["string"] } }) - // // [string, ...number[]] + // [string, ...number[]] const parsedJsonSchemaArrayFixedWithVariadic = parseJsonSchema({ type: "array", items: [{ type: "string" }], From e150832fb4ad1c81c56947a8684dff5a5e3163f4 Mon Sep 17 00:00:00 2001 From: TizzySaurus Date: Wed, 2 Oct 2024 21:39:17 +0100 Subject: [PATCH 34/61] Remove old TODO --- ark/jsonschema/json.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ark/jsonschema/json.ts b/ark/jsonschema/json.ts index 685eed5435..ff599d5c7d 100644 --- a/ark/jsonschema/json.ts +++ b/ark/jsonschema/json.ts @@ -124,7 +124,7 @@ export const innerParseJsonSchema: Type< `Provided JSON Schema must have one of 'type', 'enum', 'const', 'allOf', 'anyOf' but was ${printable(jsonSchema)}.` ) } - return preTypeValidator // TODO: Is this actually the correct thing to return??? + return preTypeValidator } ) From f625bc9c9724f27d1a1dd1ca8d4e4f8e32ca5910 Mon Sep 17 00:00:00 2001 From: TizzySaurus Date: Wed, 2 Oct 2024 21:51:33 +0100 Subject: [PATCH 35/61] Remove old comment --- ark/jsonschema/scope.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/ark/jsonschema/scope.ts b/ark/jsonschema/scope.ts index 87f6cf8810..c8a49ca8e5 100644 --- a/ark/jsonschema/scope.ts +++ b/ark/jsonschema/scope.ts @@ -4,7 +4,6 @@ const $ = scope({ AnyKeywords: { "const?": "unknown", "enum?": "unknown[]" - // "type?": "string" }, CompositionKeywords: { "allOf?": "Schema[]", From dd130e8baa151da8538fd065461e395226f7b3f8 Mon Sep 17 00:00:00 2001 From: Izan Robinson Date: Sat, 5 Oct 2024 12:13:16 +0100 Subject: [PATCH 36/61] Migrate .js imports to .ts imports --- ark/jsonschema/array.ts | 4 +- ark/jsonschema/composition.ts | 4 +- ark/jsonschema/del.ts | 485 ++++++++++++++++++++++++++++++++++ ark/jsonschema/index.ts | 4 +- ark/jsonschema/json.ts | 10 +- ark/jsonschema/number.ts | 2 +- ark/jsonschema/object.ts | 4 +- ark/jsonschema/string.ts | 2 +- 8 files changed, 500 insertions(+), 15 deletions(-) create mode 100644 ark/jsonschema/del.ts diff --git a/ark/jsonschema/array.ts b/ark/jsonschema/array.ts index 23c9bf93f7..70eeead6a1 100644 --- a/ark/jsonschema/array.ts +++ b/ark/jsonschema/array.ts @@ -7,8 +7,8 @@ import { import { printable, type array } from "@ark/util" import type { Type, applyConstraintSchema } from "arktype" -import { innerParseJsonSchema, type inferJsonSchema } from "./json.js" -import { JsonSchema } from "./scope.js" +import { innerParseJsonSchema, type inferJsonSchema } from "./json.ts" +import { JsonSchema } from "./scope.ts" const deepNormalize = (data: unknown): unknown => typeof data === "object" ? diff --git a/ark/jsonschema/composition.ts b/ark/jsonschema/composition.ts index 249f1cfd6c..94d935d253 100644 --- a/ark/jsonschema/composition.ts +++ b/ark/jsonschema/composition.ts @@ -1,7 +1,7 @@ import type { array } from "@ark/util" import { type, type Type } from "arktype" -import { innerParseJsonSchema, type inferJsonSchema } from "./json.js" -import type { JsonSchema } from "./scope.js" +import { innerParseJsonSchema, type inferJsonSchema } from "./json.ts" +import type { JsonSchema } from "./scope.ts" const validateAllOfJsonSchemas = ( jsonSchemas: JsonSchema.Schema[] diff --git a/ark/jsonschema/del.ts b/ark/jsonschema/del.ts new file mode 100644 index 0000000000..c85519ffbb --- /dev/null +++ b/ark/jsonschema/del.ts @@ -0,0 +1,485 @@ +// console.log(t.assert({ a: 3, b: 2 })) +// const t = parseJsonSchema({ +// type: "object", +// properties: { a: { type: "string" } }, +// required: ["a"], +// additionalProperties: { type: "number" } +// }) +// console.log(t.assert({ a: 3, b: 2 })) + +// const t = parseJsonSchema({ +// type: "object", +// properties: { adegf: { type: "string" } }, +// patternProperties: { "^[a-z]+$": { type: "string", minLength: 5 } } +// }) +// console.log(t.assert({ adegf: "adfgh" })) + +// const t = parseJsonSchema({ +// type: "object", +// properties: { adegf: { type: "string" } }, +// propertyNames: { type: "string", minLength: 5 } +// }) +// console.log(t.assert({ adegf: "foo", bcdge: 2 })) + +// const t = parseJsonSchema({ +// type: "object", +// properties: { +// a: { type: "string" }, +// b: { type: "number" }, +// c: { oneOf: [{ type: "string" }, { type: "number" }] } +// }, +// required: ["a", "b"], +// maxProperties: 2 +// }) +// console.log(t.assert({ a: "string", b: "stringButShouldBeNumber" })) + +// const jsonSchemaValidator = parseJsonSchema({ +// allOf: [{ type: "string" }, { const: "hello" }] +// } as const) +// const jsonSchemaValidator = parseJsonSchema({ type: "string", pattern: "hello" }) +// ^? + +// const jsonSchemaValidator = parseJsonSchema({ not: { type: "string" } }) + +// const jsonSchemaValidator = parseJsonSchema({ +// oneOf: [ +// { type: "string", maxLength: 5, minLength: 3, pattern: "foobar" }, +// { +// type: "object", +// properties: { +// foo: { type: "string" }, +// bar: { +// type: "array", +// items: { +// type: "object", +// properties: { baz: { type: "string", pattern: "baz" } } +// } +// } +// }, +// required: ["foo"] +// } +// ] +// }) +// const jsonSchemaValidator = parseJsonSchema({ +// allOf: [ +// { type: "string", maxLength: 5 }, +// { type: "string", pattern: "foo" } +// ] +// }) +// const f = jsonSchemaValidator.assert("fooba") +// console.log(f) + +// const jsonSchemaValidator = parseJsonSchema({ +// type: "number", +// maximum: 10, +// exclusiveMinimum: 4, +// multipleOf: 2 +// }) +// // ^? + +// if (jsonSchemaValidator instanceof ArkErrors) +// throw new Error(jsonSchemaValidator.summary) + +// const out = jsonSchemaValidator.assert(6) +// console.log(out) + +// +// +// +// +// + +// import type { TraversalContext } from "@arktype/schema" +// import { type, type Type } from "arktype" + +// type ExtraObjectKeywords = { +// minProperties?: number +// maxProperties?: number +// patternProperties?: [RegExp, Type][] +// propertyNamesSchema?: Type +// additionalPropertiesSchema?: Type +// } + +// const handleExtraObjectKeywords = ( +// data: object, +// ctx: TraversalContext, +// opts?: ExtraObjectKeywords +// ) => { +// const { +// minProperties = 0, +// maxProperties = Infinity, +// patternProperties = [], +// propertyNamesSchema, +// additionalPropertiesSchema +// } = opts ?? {} + +// const allKeys = Object.keys(data) +// const totalKeys = allKeys.length + +// // Validate min and max properties +// if (totalKeys < minProperties) { +// return ctx.reject({ +// message: `must be an object with at least ${minProperties} properties (had ${totalKeys})` +// }) +// } else if (totalKeys > maxProperties) { +// return ctx.reject({ +// message: `must be an object must have at most ${maxProperties} properties (had ${totalKeys})` +// }) +// } + +// // Validate property names +// if (propertyNamesSchema !== undefined) { +// for (const key of allKeys) { +// if (!propertyNamesSchema.allows(key)) { +// return ctx.reject({ +// message: `Key '${key}' must be ${propertyNamesSchema.description} due to 'propertyNames' (was ${key})` +// }) +// } +// } +// } + +// // Validate pattern properties and additional properties +// Object.entries(data).forEach(([key, value]) => { +// console.log(key) +// let didMatchAnyPatternProperty: boolean = false +// patternProperties.forEach(([pattern, schema]) => { +// if (pattern.test(key)) { +// didMatchAnyPatternProperty = true +// if (!schema.allows(value)) { +// ctx.reject({ +// path: [key], +// expected: `${schema.description} due to matching pattern property '${pattern}'`, +// actual: (value as any).toString() +// }) +// } +// } +// }) +// if (didMatchAnyPatternProperty) return + +// if ( +// additionalPropertiesSchema !== undefined && +// !["foo", "bar"].includes(key) && +// !additionalPropertiesSchema.allows(value) +// ) { +// ctx.reject({ +// path: [key], +// expected: `${additionalPropertiesSchema.description} due to being an extra property`, +// actual: value.toString() +// }) +// } +// }) + +// return true +// } + +// // const t = type({ foo: "number", "bar?": "string" }).narrow((data, ctx) => +// // handleExtraObjectKeywords(data, ctx, { +// // additionalPropertiesSchema: type("number") +// // }) +// // ) + +// // console.log( +// // t.assert({ +// // foo: 3, +// // bar: 3, +// // baz: 3, +// // bad: "3" +// // }) +// // ) + +// const t = type({ "[string<2]": "number" }).onUndeclaredKey("reject") +// console.log(t.assert({ foo: 3, bar: 3, baz: 3, bad: 4 })) +// message: `Key '${key}' must be ${propertyNamesSchema.description} due to 'propertyNames' (was ${key})` +// }) +// } +// } +// } + +// // Validate pattern properties and additional properties +// Object.entries(data).forEach(([key, value]) => { +// console.log(key) +// let didMatchAnyPatternProperty: boolean = false +// patternProperties.forEach(([pattern, schema]) => { +// if (pattern.test(key)) { +// didMatchAnyPatternProperty = true +// if (!schema.allows(value)) { +// ctx.reject({ +// path: [key], +// expected: `${schema.description} due to matching pattern property '${pattern}'`, +// actual: (value as any).toString() +// }) +// } +// } +// }) +// if (didMatchAnyPatternProperty) return + +// if ( +// additionalPropertiesSchema !== undefined && +// !["foo", "bar"].includes(key) && +// !additionalPropertiesSchema.allows(value) +// ) { +// ctx.reject({ +// path: [key], +// expected: `${additionalPropertiesSchema.description} due to being an extra property`, +// actual: value.toString() +// }) +// } +// }) + +// return true +// } + +// // const t = type({ foo: "number", "bar?": "string" }).narrow((data, ctx) => +// // handleExtraObjectKeywords(data, ctx, { +// // additionalPropertiesSchema: type("number") +// // }) +// // ) + +// // console.log( +// // t.assert({ +// // foo: 3, +// // bar: 3, +// // baz: 3, +// // bad: "3" +// // }) +// // ) + +// const t = type({ "[string<2]": "number" }).onUndeclaredKey("reject") +// console.log(t.assert({ foo: 3, bar: 3, baz: 3, bad: 4 })) +// message: `Key '${key}' must be ${propertyNamesSchema.description} due to 'propertyNames' (was ${key})` +// }) +// } +// } +// } + +// // Validate pattern properties and additional properties +// Object.entries(data).forEach(([key, value]) => { +// console.log(key) +// let didMatchAnyPatternProperty: boolean = false +// patternProperties.forEach(([pattern, schema]) => { +// if (pattern.test(key)) { +// didMatchAnyPatternProperty = true +// if (!schema.allows(value)) { +// ctx.reject({ +// path: [key], +// expected: `${schema.description} due to matching pattern property '${pattern}'`, +// actual: (value as any).toString() +// }) +// } +// } +// }) +// if (didMatchAnyPatternProperty) return + +// if ( +// additionalPropertiesSchema !== undefined && +// !["foo", "bar"].includes(key) && +// !additionalPropertiesSchema.allows(value) +// ) { +// ctx.reject({ +// path: [key], +// expected: `${additionalPropertiesSchema.description} due to being an extra property`, +// actual: value.toString() +// }) +// } +// }) + +// return true +// } + +// // const t = type({ foo: "number", "bar?": "string" }).narrow((data, ctx) => +// // handleExtraObjectKeywords(data, ctx, { +// // additionalPropertiesSchema: type("number") +// // }) +// // ) + +// // console.log( +// // t.assert({ +// // foo: 3, +// // bar: 3, +// // baz: 3, +// // bad: "3" +// // }) +// // ) + +// const t = type({ "[string<2]": "number" }).onUndeclaredKey("reject") +// console.log(t.assert({ foo: 3, bar: 3, baz: 3, bad: 4 })) +// message: `Key '${key}' must be ${propertyNamesSchema.description} due to 'propertyNames' (was ${key})` +// }) +// } +// } +// } + +// // Validate pattern properties and additional properties +// Object.entries(data).forEach(([key, value]) => { +// console.log(key) +// let didMatchAnyPatternProperty: boolean = false +// patternProperties.forEach(([pattern, schema]) => { +// if (pattern.test(key)) { +// didMatchAnyPatternProperty = true +// if (!schema.allows(value)) { +// ctx.reject({ +// path: [key], +// expected: `${schema.description} due to matching pattern property '${pattern}'`, +// actual: (value as any).toString() +// }) +// } +// } +// }) +// if (didMatchAnyPatternProperty) return + +// if ( +// additionalPropertiesSchema !== undefined && +// !["foo", "bar"].includes(key) && +// !additionalPropertiesSchema.allows(value) +// ) { +// ctx.reject({ +// path: [key], +// expected: `${additionalPropertiesSchema.description} due to being an extra property`, +// actual: value.toString() +// }) +// } +// }) + +// return true +// } + +// // const t = type({ foo: "number", "bar?": "string" }).narrow((data, ctx) => +// // handleExtraObjectKeywords(data, ctx, { +// // additionalPropertiesSchema: type("number") +// // }) +// // ) + +// // console.log( +// // t.assert({ +// // foo: 3, +// // bar: 3, +// // baz: 3, +// // bad: "3" +// // }) +// // ) + +// const t = type({ "[string<2]": "number" }).onUndeclaredKey("reject") +// console.log(t.assert({ foo: 3, bar: 3, baz: 3, bad: 4 })) + +// +// +// +// +// + +// type f = inferJsonSchemaArray<{ +// type: "array" +// items: [{ type: "string" }, { type: "number" }] +// additionalItems: { type: "boolean" } +// }> +// // ^? + +// type g = inferJsonSchemaArray<{ +// type: "array" +// items: { type: "string" } +// additionalItems: { type: "number" } +// }> +// // ^? + +// type h = inferJsonSchemaArray<{ +// type: "array" +// items: [] +// additionalItems: { type: "number" } +// }> +// // ^? + +// +// +// + +// type("15") + +// const t1 = type({ "[string]": t0 }) +// console.log(JSON.stringify(t1.json)) +// +// +// + +// import { +// type, +// type AtMostLength, +// type Type, +// type applyConstraint +// } from "arktype" + +// const t = type("unknown[]>5") +// type t = (typeof t)["infer"] + +// type f = Type> +// type g = f["infer"] +// // ^? + +// +// +// + +// import { rootNode } from "@ark/schema"; +// import type { Type, applyConstraint } from "arktype"; + +// type f = Type, "minLength", { rule: 3 }>> +// // ^? + +// +// +// + +// import { type } from "arktype" + +// const obj = { foo: 3 } + +// const t = type({ +// foo: "number" +// }).pipe(o => ({ a: o })) + +// const a = t(obj) +// console.log(a) // { foo: 6 } +// console.log(obj) // { foo: 3 } + +// import { type } from "arktype" + +// const t = type({ +// "optionalKey?": ["string", "=>", x => x.toLowerCase()], +// requiredKey: ["string", "=>", x => x.toLowerCase()] +// }) + +// const a = t.assert({ optionalKey: "Hi", requiredKey: "Hi" }) +// console.log(a) + +// +// + +// import { scope } from "arktype" + +// const $ = scope({ +// TypeWithNoKeywords: { type: "'boolean'|'null'" }, +// TypeWithKeywords: "ArraySchema|ObjectSchema", // without both of these there's no error +// // "#BaseSchema": "TypeWithNoKeywords|boolean", // errors even with union reversed +// "#BaseSchema": "boolean|TypeWithNoKeywords", // without the `boolean` there's no error (even if still union such as `string|TypeWithNoKeywords`) +// ArraySchema: { +// "additionalItems?": "BaseSchema", // without this recursion there's no error +// type: "'array'" +// }, +// // If `ObjectSchema` isn't an object, there's no error +// // E.g. `ObjectSchema: "string[]"` is fine +// ObjectSchema: { +// type: "'object'" +// } +// }) +// export const JsonSchema = $.export() // TypeError: Cannot use 'in' operator to search for 'type' in false + +// +// + +// import { type } from "arktype" + +// const t = type(["parse.integer", "=>", n => n >= 5]) +// console.log(t.assert("5")) diff --git a/ark/jsonschema/index.ts b/ark/jsonschema/index.ts index 0b23b9ea49..7363a05a1c 100644 --- a/ark/jsonschema/index.ts +++ b/ark/jsonschema/index.ts @@ -1,2 +1,2 @@ -export { parseJsonSchema } from "./json.js" -export * from "./scope.js" +export { parseJsonSchema } from "./json.ts" +export * from "./scope.ts" diff --git a/ark/jsonschema/json.ts b/ark/jsonschema/json.ts index ff599d5c7d..2b1a64d1e4 100644 --- a/ark/jsonschema/json.ts +++ b/ark/jsonschema/json.ts @@ -6,7 +6,7 @@ import { } from "@ark/util" import { type, type Out, type Type } from "arktype" import { parseJsonSchemaAnyKeywords } from "./any.ts" -import { validateJsonSchemaArray, type inferJsonSchemaArray } from "./array.js" +import { validateJsonSchemaArray, type inferJsonSchemaArray } from "./array.ts" import { parseJsonSchemaCompositionKeywords, type inferJsonSchemaComposition @@ -14,16 +14,16 @@ import { import { validateJsonSchemaNumber, type inferJsonSchemaNumber -} from "./number.js" +} from "./number.ts" import { validateJsonSchemaObject, type inferJsonSchemaObject -} from "./object.js" -import { JsonSchema } from "./scope.js" +} from "./object.ts" +import { JsonSchema } from "./scope.ts" import { validateJsonSchemaString, type inferJsonSchemaString -} from "./string.js" +} from "./string.ts" type JsonSchemaConstraintKind = "const" | "enum" type JsonSchemaConst = { const: t } diff --git a/ark/jsonschema/number.ts b/ark/jsonschema/number.ts index 9da6eb9194..0303cb7f9a 100644 --- a/ark/jsonschema/number.ts +++ b/ark/jsonschema/number.ts @@ -1,7 +1,7 @@ import { rootSchema, type Intersection } from "@ark/schema" import { throwParseError } from "@ark/util" import type { Type, number } from "arktype" -import { JsonSchema } from "./scope.js" +import { JsonSchema } from "./scope.ts" export const validateJsonSchemaNumber = JsonSchema.NumberSchema.pipe( jsonSchema => { diff --git a/ark/jsonschema/object.ts b/ark/jsonschema/object.ts index 50de84dc99..390e71cefb 100644 --- a/ark/jsonschema/object.ts +++ b/ark/jsonschema/object.ts @@ -8,8 +8,8 @@ import { import { printable, type show } from "@ark/util" import type { Type } from "arktype" -import { innerParseJsonSchema, type inferJsonSchema } from "./json.js" -import { JsonSchema } from "./scope.js" +import { innerParseJsonSchema, type inferJsonSchema } from "./json.ts" +import { JsonSchema } from "./scope.ts" const parseMinMaxProperties = ( jsonSchema: JsonSchema.ObjectSchema, diff --git a/ark/jsonschema/string.ts b/ark/jsonschema/string.ts index ad2ec33ae7..62127c5735 100644 --- a/ark/jsonschema/string.ts +++ b/ark/jsonschema/string.ts @@ -1,6 +1,6 @@ import { rootSchema, type Intersection } from "@ark/schema" import type { Type, string } from "arktype" -import { JsonSchema } from "./scope.js" +import { JsonSchema } from "./scope.ts" export const validateJsonSchemaString = JsonSchema.StringSchema.pipe( jsonSchema => { From 4598d020b7e4df412f8e6868aaec943afafcfc5d Mon Sep 17 00:00:00 2001 From: Izan Robinson Date: Sat, 5 Oct 2024 12:14:34 +0100 Subject: [PATCH 37/61] Remove accidentally added ark/jsonschema/del.ts file --- ark/jsonschema/del.ts | 485 ------------------------------------------ 1 file changed, 485 deletions(-) delete mode 100644 ark/jsonschema/del.ts diff --git a/ark/jsonschema/del.ts b/ark/jsonschema/del.ts deleted file mode 100644 index c85519ffbb..0000000000 --- a/ark/jsonschema/del.ts +++ /dev/null @@ -1,485 +0,0 @@ -// console.log(t.assert({ a: 3, b: 2 })) -// const t = parseJsonSchema({ -// type: "object", -// properties: { a: { type: "string" } }, -// required: ["a"], -// additionalProperties: { type: "number" } -// }) -// console.log(t.assert({ a: 3, b: 2 })) - -// const t = parseJsonSchema({ -// type: "object", -// properties: { adegf: { type: "string" } }, -// patternProperties: { "^[a-z]+$": { type: "string", minLength: 5 } } -// }) -// console.log(t.assert({ adegf: "adfgh" })) - -// const t = parseJsonSchema({ -// type: "object", -// properties: { adegf: { type: "string" } }, -// propertyNames: { type: "string", minLength: 5 } -// }) -// console.log(t.assert({ adegf: "foo", bcdge: 2 })) - -// const t = parseJsonSchema({ -// type: "object", -// properties: { -// a: { type: "string" }, -// b: { type: "number" }, -// c: { oneOf: [{ type: "string" }, { type: "number" }] } -// }, -// required: ["a", "b"], -// maxProperties: 2 -// }) -// console.log(t.assert({ a: "string", b: "stringButShouldBeNumber" })) - -// const jsonSchemaValidator = parseJsonSchema({ -// allOf: [{ type: "string" }, { const: "hello" }] -// } as const) -// const jsonSchemaValidator = parseJsonSchema({ type: "string", pattern: "hello" }) -// ^? - -// const jsonSchemaValidator = parseJsonSchema({ not: { type: "string" } }) - -// const jsonSchemaValidator = parseJsonSchema({ -// oneOf: [ -// { type: "string", maxLength: 5, minLength: 3, pattern: "foobar" }, -// { -// type: "object", -// properties: { -// foo: { type: "string" }, -// bar: { -// type: "array", -// items: { -// type: "object", -// properties: { baz: { type: "string", pattern: "baz" } } -// } -// } -// }, -// required: ["foo"] -// } -// ] -// }) -// const jsonSchemaValidator = parseJsonSchema({ -// allOf: [ -// { type: "string", maxLength: 5 }, -// { type: "string", pattern: "foo" } -// ] -// }) -// const f = jsonSchemaValidator.assert("fooba") -// console.log(f) - -// const jsonSchemaValidator = parseJsonSchema({ -// type: "number", -// maximum: 10, -// exclusiveMinimum: 4, -// multipleOf: 2 -// }) -// // ^? - -// if (jsonSchemaValidator instanceof ArkErrors) -// throw new Error(jsonSchemaValidator.summary) - -// const out = jsonSchemaValidator.assert(6) -// console.log(out) - -// -// -// -// -// - -// import type { TraversalContext } from "@arktype/schema" -// import { type, type Type } from "arktype" - -// type ExtraObjectKeywords = { -// minProperties?: number -// maxProperties?: number -// patternProperties?: [RegExp, Type][] -// propertyNamesSchema?: Type -// additionalPropertiesSchema?: Type -// } - -// const handleExtraObjectKeywords = ( -// data: object, -// ctx: TraversalContext, -// opts?: ExtraObjectKeywords -// ) => { -// const { -// minProperties = 0, -// maxProperties = Infinity, -// patternProperties = [], -// propertyNamesSchema, -// additionalPropertiesSchema -// } = opts ?? {} - -// const allKeys = Object.keys(data) -// const totalKeys = allKeys.length - -// // Validate min and max properties -// if (totalKeys < minProperties) { -// return ctx.reject({ -// message: `must be an object with at least ${minProperties} properties (had ${totalKeys})` -// }) -// } else if (totalKeys > maxProperties) { -// return ctx.reject({ -// message: `must be an object must have at most ${maxProperties} properties (had ${totalKeys})` -// }) -// } - -// // Validate property names -// if (propertyNamesSchema !== undefined) { -// for (const key of allKeys) { -// if (!propertyNamesSchema.allows(key)) { -// return ctx.reject({ -// message: `Key '${key}' must be ${propertyNamesSchema.description} due to 'propertyNames' (was ${key})` -// }) -// } -// } -// } - -// // Validate pattern properties and additional properties -// Object.entries(data).forEach(([key, value]) => { -// console.log(key) -// let didMatchAnyPatternProperty: boolean = false -// patternProperties.forEach(([pattern, schema]) => { -// if (pattern.test(key)) { -// didMatchAnyPatternProperty = true -// if (!schema.allows(value)) { -// ctx.reject({ -// path: [key], -// expected: `${schema.description} due to matching pattern property '${pattern}'`, -// actual: (value as any).toString() -// }) -// } -// } -// }) -// if (didMatchAnyPatternProperty) return - -// if ( -// additionalPropertiesSchema !== undefined && -// !["foo", "bar"].includes(key) && -// !additionalPropertiesSchema.allows(value) -// ) { -// ctx.reject({ -// path: [key], -// expected: `${additionalPropertiesSchema.description} due to being an extra property`, -// actual: value.toString() -// }) -// } -// }) - -// return true -// } - -// // const t = type({ foo: "number", "bar?": "string" }).narrow((data, ctx) => -// // handleExtraObjectKeywords(data, ctx, { -// // additionalPropertiesSchema: type("number") -// // }) -// // ) - -// // console.log( -// // t.assert({ -// // foo: 3, -// // bar: 3, -// // baz: 3, -// // bad: "3" -// // }) -// // ) - -// const t = type({ "[string<2]": "number" }).onUndeclaredKey("reject") -// console.log(t.assert({ foo: 3, bar: 3, baz: 3, bad: 4 })) -// message: `Key '${key}' must be ${propertyNamesSchema.description} due to 'propertyNames' (was ${key})` -// }) -// } -// } -// } - -// // Validate pattern properties and additional properties -// Object.entries(data).forEach(([key, value]) => { -// console.log(key) -// let didMatchAnyPatternProperty: boolean = false -// patternProperties.forEach(([pattern, schema]) => { -// if (pattern.test(key)) { -// didMatchAnyPatternProperty = true -// if (!schema.allows(value)) { -// ctx.reject({ -// path: [key], -// expected: `${schema.description} due to matching pattern property '${pattern}'`, -// actual: (value as any).toString() -// }) -// } -// } -// }) -// if (didMatchAnyPatternProperty) return - -// if ( -// additionalPropertiesSchema !== undefined && -// !["foo", "bar"].includes(key) && -// !additionalPropertiesSchema.allows(value) -// ) { -// ctx.reject({ -// path: [key], -// expected: `${additionalPropertiesSchema.description} due to being an extra property`, -// actual: value.toString() -// }) -// } -// }) - -// return true -// } - -// // const t = type({ foo: "number", "bar?": "string" }).narrow((data, ctx) => -// // handleExtraObjectKeywords(data, ctx, { -// // additionalPropertiesSchema: type("number") -// // }) -// // ) - -// // console.log( -// // t.assert({ -// // foo: 3, -// // bar: 3, -// // baz: 3, -// // bad: "3" -// // }) -// // ) - -// const t = type({ "[string<2]": "number" }).onUndeclaredKey("reject") -// console.log(t.assert({ foo: 3, bar: 3, baz: 3, bad: 4 })) -// message: `Key '${key}' must be ${propertyNamesSchema.description} due to 'propertyNames' (was ${key})` -// }) -// } -// } -// } - -// // Validate pattern properties and additional properties -// Object.entries(data).forEach(([key, value]) => { -// console.log(key) -// let didMatchAnyPatternProperty: boolean = false -// patternProperties.forEach(([pattern, schema]) => { -// if (pattern.test(key)) { -// didMatchAnyPatternProperty = true -// if (!schema.allows(value)) { -// ctx.reject({ -// path: [key], -// expected: `${schema.description} due to matching pattern property '${pattern}'`, -// actual: (value as any).toString() -// }) -// } -// } -// }) -// if (didMatchAnyPatternProperty) return - -// if ( -// additionalPropertiesSchema !== undefined && -// !["foo", "bar"].includes(key) && -// !additionalPropertiesSchema.allows(value) -// ) { -// ctx.reject({ -// path: [key], -// expected: `${additionalPropertiesSchema.description} due to being an extra property`, -// actual: value.toString() -// }) -// } -// }) - -// return true -// } - -// // const t = type({ foo: "number", "bar?": "string" }).narrow((data, ctx) => -// // handleExtraObjectKeywords(data, ctx, { -// // additionalPropertiesSchema: type("number") -// // }) -// // ) - -// // console.log( -// // t.assert({ -// // foo: 3, -// // bar: 3, -// // baz: 3, -// // bad: "3" -// // }) -// // ) - -// const t = type({ "[string<2]": "number" }).onUndeclaredKey("reject") -// console.log(t.assert({ foo: 3, bar: 3, baz: 3, bad: 4 })) -// message: `Key '${key}' must be ${propertyNamesSchema.description} due to 'propertyNames' (was ${key})` -// }) -// } -// } -// } - -// // Validate pattern properties and additional properties -// Object.entries(data).forEach(([key, value]) => { -// console.log(key) -// let didMatchAnyPatternProperty: boolean = false -// patternProperties.forEach(([pattern, schema]) => { -// if (pattern.test(key)) { -// didMatchAnyPatternProperty = true -// if (!schema.allows(value)) { -// ctx.reject({ -// path: [key], -// expected: `${schema.description} due to matching pattern property '${pattern}'`, -// actual: (value as any).toString() -// }) -// } -// } -// }) -// if (didMatchAnyPatternProperty) return - -// if ( -// additionalPropertiesSchema !== undefined && -// !["foo", "bar"].includes(key) && -// !additionalPropertiesSchema.allows(value) -// ) { -// ctx.reject({ -// path: [key], -// expected: `${additionalPropertiesSchema.description} due to being an extra property`, -// actual: value.toString() -// }) -// } -// }) - -// return true -// } - -// // const t = type({ foo: "number", "bar?": "string" }).narrow((data, ctx) => -// // handleExtraObjectKeywords(data, ctx, { -// // additionalPropertiesSchema: type("number") -// // }) -// // ) - -// // console.log( -// // t.assert({ -// // foo: 3, -// // bar: 3, -// // baz: 3, -// // bad: "3" -// // }) -// // ) - -// const t = type({ "[string<2]": "number" }).onUndeclaredKey("reject") -// console.log(t.assert({ foo: 3, bar: 3, baz: 3, bad: 4 })) - -// -// -// -// -// - -// type f = inferJsonSchemaArray<{ -// type: "array" -// items: [{ type: "string" }, { type: "number" }] -// additionalItems: { type: "boolean" } -// }> -// // ^? - -// type g = inferJsonSchemaArray<{ -// type: "array" -// items: { type: "string" } -// additionalItems: { type: "number" } -// }> -// // ^? - -// type h = inferJsonSchemaArray<{ -// type: "array" -// items: [] -// additionalItems: { type: "number" } -// }> -// // ^? - -// -// -// - -// type("15") - -// const t1 = type({ "[string]": t0 }) -// console.log(JSON.stringify(t1.json)) -// -// -// - -// import { -// type, -// type AtMostLength, -// type Type, -// type applyConstraint -// } from "arktype" - -// const t = type("unknown[]>5") -// type t = (typeof t)["infer"] - -// type f = Type> -// type g = f["infer"] -// // ^? - -// -// -// - -// import { rootNode } from "@ark/schema"; -// import type { Type, applyConstraint } from "arktype"; - -// type f = Type, "minLength", { rule: 3 }>> -// // ^? - -// -// -// - -// import { type } from "arktype" - -// const obj = { foo: 3 } - -// const t = type({ -// foo: "number" -// }).pipe(o => ({ a: o })) - -// const a = t(obj) -// console.log(a) // { foo: 6 } -// console.log(obj) // { foo: 3 } - -// import { type } from "arktype" - -// const t = type({ -// "optionalKey?": ["string", "=>", x => x.toLowerCase()], -// requiredKey: ["string", "=>", x => x.toLowerCase()] -// }) - -// const a = t.assert({ optionalKey: "Hi", requiredKey: "Hi" }) -// console.log(a) - -// -// - -// import { scope } from "arktype" - -// const $ = scope({ -// TypeWithNoKeywords: { type: "'boolean'|'null'" }, -// TypeWithKeywords: "ArraySchema|ObjectSchema", // without both of these there's no error -// // "#BaseSchema": "TypeWithNoKeywords|boolean", // errors even with union reversed -// "#BaseSchema": "boolean|TypeWithNoKeywords", // without the `boolean` there's no error (even if still union such as `string|TypeWithNoKeywords`) -// ArraySchema: { -// "additionalItems?": "BaseSchema", // without this recursion there's no error -// type: "'array'" -// }, -// // If `ObjectSchema` isn't an object, there's no error -// // E.g. `ObjectSchema: "string[]"` is fine -// ObjectSchema: { -// type: "'object'" -// } -// }) -// export const JsonSchema = $.export() // TypeError: Cannot use 'in' operator to search for 'type' in false - -// -// - -// import { type } from "arktype" - -// const t = type(["parse.integer", "=>", n => n >= 5]) -// console.log(t.assert("5")) From f7bd853204a5b97bca6f5d36b50302856aead5c4 Mon Sep 17 00:00:00 2001 From: TizzySaurus Date: Sun, 13 Oct 2024 11:07:45 +0100 Subject: [PATCH 38/61] Remove changeset --- .changeset/brave-plums-clap.md | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 .changeset/brave-plums-clap.md diff --git a/.changeset/brave-plums-clap.md b/.changeset/brave-plums-clap.md deleted file mode 100644 index 15d5a61d97..0000000000 --- a/.changeset/brave-plums-clap.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"@arktype/schema": patch ---- - -(see [arktype CHANGELOG](../type/CHANGELOG.md)) - -### Fix a ParseError compiling certain morphs with cyclic inputs - -### Rename RegexNode to PatternNode From 03ba515efd44b9e18d402a5c58d6b5e0615597b2 Mon Sep 17 00:00:00 2001 From: TizzySaurus Date: Sun, 13 Oct 2024 11:08:37 +0100 Subject: [PATCH 39/61] Use type.enumerated and type.unit utils for 'const' and 'enum' JSON Schema keywords --- ark/jsonschema/any.ts | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/ark/jsonschema/any.ts b/ark/jsonschema/any.ts index 975d3b5c9d..c2c1d99505 100644 --- a/ark/jsonschema/any.ts +++ b/ark/jsonschema/any.ts @@ -1,6 +1,5 @@ -import { rootSchema } from "@ark/schema" import { throwParseError } from "@ark/util" -import type { Type } from "arktype" +import { type Type, type } from "arktype" import type { JsonSchema } from "./scope.ts" export const parseJsonSchemaAnyKeywords = ( @@ -16,14 +15,8 @@ export const parseJsonSchemaAnyKeywords = ( "Provided JSON Schema cannot have both 'const' and 'enum' keywords." ) } - return rootSchema({ unit: jsonSchema.const }) as unknown as Type + return type.unit(jsonSchema.const) } - if ("enum" in jsonSchema) { - return rootSchema( - jsonSchema.enum.map((unit: unknown) => ({ - unit - })) - ) as unknown as Type - } + if ("enum" in jsonSchema) return type.enumerated(jsonSchema.enum) } From 01e296c238b7172fa41c244c21b0b1560a41e634 Mon Sep 17 00:00:00 2001 From: TizzySaurus Date: Sun, 13 Oct 2024 11:09:27 +0100 Subject: [PATCH 40/61] Specify return type of function rather than double casting the return value --- ark/jsonschema/array.ts | 4 ++-- ark/jsonschema/number.ts | 4 ++-- ark/jsonschema/string.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ark/jsonschema/array.ts b/ark/jsonschema/array.ts index 23c9bf93f7..a918077d17 100644 --- a/ark/jsonschema/array.ts +++ b/ark/jsonschema/array.ts @@ -52,7 +52,7 @@ const arrayContainsItemMatchingSchema = ( ) export const validateJsonSchemaArray = JsonSchema.ArraySchema.pipe( - jsonSchema => { + (jsonSchema): Type => { const arktypeArraySchema: Intersection.Schema> = { proto: "Array" } @@ -103,7 +103,7 @@ export const validateJsonSchemaArray = JsonSchema.ArraySchema.pipe( arktypeArraySchema.predicate = predicates - return rootSchema(arktypeArraySchema) as unknown as Type + return rootSchema(arktypeArraySchema) as never } ) diff --git a/ark/jsonschema/number.ts b/ark/jsonschema/number.ts index 9da6eb9194..afb1cbde80 100644 --- a/ark/jsonschema/number.ts +++ b/ark/jsonschema/number.ts @@ -4,7 +4,7 @@ import type { Type, number } from "arktype" import { JsonSchema } from "./scope.js" export const validateJsonSchemaNumber = JsonSchema.NumberSchema.pipe( - jsonSchema => { + (jsonSchema): Type => { const arktypeNumberSchema: Intersection.Schema = { domain: "number" } @@ -41,7 +41,7 @@ export const validateJsonSchemaNumber = JsonSchema.NumberSchema.pipe( arktypeNumberSchema.divisor = jsonSchema.multipleOf else if (jsonSchema.type === "integer") arktypeNumberSchema.divisor = 1 - return rootSchema(arktypeNumberSchema) as unknown as Type + return rootSchema(arktypeNumberSchema) as never } ) diff --git a/ark/jsonschema/string.ts b/ark/jsonschema/string.ts index ad2ec33ae7..b20dcc1611 100644 --- a/ark/jsonschema/string.ts +++ b/ark/jsonschema/string.ts @@ -3,7 +3,7 @@ import type { Type, string } from "arktype" import { JsonSchema } from "./scope.js" export const validateJsonSchemaString = JsonSchema.StringSchema.pipe( - jsonSchema => { + (jsonSchema): Type => { const arktypeStringSchema: Intersection.Schema = { domain: "string" } @@ -20,7 +20,7 @@ export const validateJsonSchemaString = JsonSchema.StringSchema.pipe( ] } else arktypeStringSchema.pattern = [jsonSchema.pattern] } - return rootSchema(arktypeStringSchema) as unknown as Type + return rootSchema(arktypeStringSchema) as never } ) From dc43e17a6ef014238eea07447c9c4729506559d5 Mon Sep 17 00:00:00 2001 From: TizzySaurus Date: Sun, 13 Oct 2024 11:10:04 +0100 Subject: [PATCH 41/61] Make variable assignment clearer & remove debug log statement --- ark/jsonschema/object.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ark/jsonschema/object.ts b/ark/jsonschema/object.ts index 50de84dc99..c8fe6795e6 100644 --- a/ark/jsonschema/object.ts +++ b/ark/jsonschema/object.ts @@ -190,7 +190,7 @@ const parseRequiredAndOptionalKeys = ( const parseAdditionalProperties = (jsonSchema: JsonSchema.ObjectSchema) => { if (!("additionalProperties" in jsonSchema)) return - const properties = Object.keys(jsonSchema.properties ?? {}) + const properties = jsonSchema.properties ? Object.keys(jsonSchema.properties) : [] const patternProperties = Object.keys(jsonSchema.patternProperties ?? {}) const additionalPropertiesSchema = jsonSchema.additionalProperties @@ -254,7 +254,6 @@ export const validateJsonSchemaObject = JsonSchema.ObjectSchema.pipe( ].filter(x => x !== undefined) const typeWithoutPredicates = rootSchema(arktypeObjectSchema) - console.log(typeWithoutPredicates.json) if (predicates.length === 0) return typeWithoutPredicates as never return rootSchema({ domain: "object", predicate: predicates }).narrow( From b6e5c70c587975e236da26fc5f005b872eb5aa5a Mon Sep 17 00:00:00 2001 From: TizzySaurus Date: Sun, 13 Oct 2024 11:21:04 +0100 Subject: [PATCH 42/61] Update @ark/jsonschema 'scripts' and 'exports' to match new style in repo --- ark/jsonschema/package.json | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/ark/jsonschema/package.json b/ark/jsonschema/package.json index 23c188fd0a..7ffc255029 100644 --- a/ark/jsonschema/package.json +++ b/ark/jsonschema/package.json @@ -15,17 +15,23 @@ "main": "./out/index.js", "types": "./out/index.d.ts", "exports": { - ".": "./out/index.js", - "./internal/*": "./out/*" + ".": { + "ark-ts": "./index.ts", + "default": "./out/index.js" + }, + "./internal/*.ts": { + "ark-ts": "./*.ts", + "default": "./out/*.js" + } }, "files": [ "out" ], "scripts": { - "build": "tsx ../repo/build.ts", - "bench": "tsx ./__tests__/comparison.bench.ts", - "test": "tsx ../repo/testPackage.ts", - "tnt": "tsx ../repo/testPackage.ts --skipTypes" + "build": "ts ../repo/build.ts", + "bench": "ts ./__tests__/comparison.bench.ts", + "test": "ts ../repo/testPackage.ts", + "tnt": "ts ../repo/testPackage.ts --skipTypes" }, "dependencies": { "arktype": "workspace:*", From 423ba89ae524ee118696aa572d2226782ffa183a Mon Sep 17 00:00:00 2001 From: TizzySaurus Date: Sun, 13 Oct 2024 11:28:59 +0100 Subject: [PATCH 43/61] Formatting --- ark/jsonschema/object.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ark/jsonschema/object.ts b/ark/jsonschema/object.ts index 4fd10da230..071ecb2037 100644 --- a/ark/jsonschema/object.ts +++ b/ark/jsonschema/object.ts @@ -190,7 +190,8 @@ const parseRequiredAndOptionalKeys = ( const parseAdditionalProperties = (jsonSchema: JsonSchema.ObjectSchema) => { if (!("additionalProperties" in jsonSchema)) return - const properties = jsonSchema.properties ? Object.keys(jsonSchema.properties) : [] + const properties = + jsonSchema.properties ? Object.keys(jsonSchema.properties) : [] const patternProperties = Object.keys(jsonSchema.patternProperties ?? {}) const additionalPropertiesSchema = jsonSchema.additionalProperties From 2fe1b7acccd44a133f290d021b08e3418a6e0732 Mon Sep 17 00:00:00 2001 From: TizzySaurus Date: Sun, 13 Oct 2024 11:36:44 +0100 Subject: [PATCH 44/61] Use conflatenateAll util instead of manually filtering out undefined values --- ark/jsonschema/del.ts | 5 +++++ ark/jsonschema/object.ts | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 ark/jsonschema/del.ts diff --git a/ark/jsonschema/del.ts b/ark/jsonschema/del.ts new file mode 100644 index 0000000000..4f6f605be9 --- /dev/null +++ b/ark/jsonschema/del.ts @@ -0,0 +1,5 @@ +import { conflatenate, conflatenateAll } from "@ark/util" + +// const arr = conflatenate([], 2) +const arr = conflatenateAll(undefined, 2, undefined, 3, 1) +console.log(arr) diff --git a/ark/jsonschema/object.ts b/ark/jsonschema/object.ts index 071ecb2037..f30a8524e8 100644 --- a/ark/jsonschema/object.ts +++ b/ark/jsonschema/object.ts @@ -5,7 +5,7 @@ import { type Predicate, type TraversalContext } from "@ark/schema" -import { printable, type show } from "@ark/util" +import { conflatenateAll, printable, type show } from "@ark/util" import type { Type } from "arktype" import { innerParseJsonSchema, type inferJsonSchema } from "./json.ts" @@ -247,12 +247,12 @@ export const validateJsonSchemaObject = JsonSchema.ObjectSchema.pipe( arktypeObjectSchema.required = requiredKeys arktypeObjectSchema.optional = optionalKeys - const predicates: Predicate.Schema[] = [ + const predicates = conflatenateAll( ...parseMinMaxProperties(jsonSchema, ctx), parsePropertyNames(jsonSchema, ctx), parsePatternProperties(jsonSchema, ctx), parseAdditionalProperties(jsonSchema) - ].filter(x => x !== undefined) + ) const typeWithoutPredicates = rootSchema(arktypeObjectSchema) if (predicates.length === 0) return typeWithoutPredicates as never From d0e612b59b7239f8d2826459b8922ce5d8edd89a Mon Sep 17 00:00:00 2001 From: TizzySaurus Date: Sun, 13 Oct 2024 11:37:59 +0100 Subject: [PATCH 45/61] Remove accidentally added debugging file --- ark/jsonschema/del.ts | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 ark/jsonschema/del.ts diff --git a/ark/jsonschema/del.ts b/ark/jsonschema/del.ts deleted file mode 100644 index 4f6f605be9..0000000000 --- a/ark/jsonschema/del.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { conflatenate, conflatenateAll } from "@ark/util" - -// const arr = conflatenate([], 2) -const arr = conflatenateAll(undefined, 2, undefined, 3, 1) -console.log(arr) From 07a5215d314a6b39471eeaaed211f0956c694828 Mon Sep 17 00:00:00 2001 From: TizzySaurus Date: Fri, 25 Oct 2024 22:11:12 +0100 Subject: [PATCH 46/61] Remove type inference from @ark/jsonschema --- ark/jsonschema/__tests__/array.test.ts | 7 --- ark/jsonschema/__tests__/json.test.ts | 22 -------- ark/jsonschema/__tests__/number.test.ts | 8 --- ark/jsonschema/__tests__/object.test.ts | 6 --- ark/jsonschema/__tests__/string.test.ts | 5 -- ark/jsonschema/array.ts | 57 ++------------------ ark/jsonschema/composition.ts | 29 +--------- ark/jsonschema/json.ts | 72 +++---------------------- ark/jsonschema/number.ts | 37 +------------ ark/jsonschema/object.ts | 69 +----------------------- ark/jsonschema/string.ts | 20 +------ 11 files changed, 16 insertions(+), 316 deletions(-) diff --git a/ark/jsonschema/__tests__/array.test.ts b/ark/jsonschema/__tests__/array.test.ts index 09d8f6d800..29f0670cba 100644 --- a/ark/jsonschema/__tests__/array.test.ts +++ b/ark/jsonschema/__tests__/array.test.ts @@ -7,7 +7,6 @@ import { parseJsonSchema } from "@ark/jsonschema" contextualize(() => { it("type array", () => { const t = parseJsonSchema({ type: "array" }) - attest(t.infer) attest(t.json).snap({ proto: "Array" }) }) @@ -16,7 +15,6 @@ contextualize(() => { type: "array", items: [{ type: "string" }, { type: "number" }] }) - attest<[string, number]>(tItems.infer) attest(tItems.json).snap({ proto: "Array", sequence: { prefix: ["string", "number"] }, @@ -31,7 +29,6 @@ contextualize(() => { items: [{ type: "string" }, { type: "number" }], additionalItems: { type: "boolean" } }) - attest<[string, number, ...boolean[]]>(tItemsVariadic.infer) attest(tItemsVariadic.json).snap({ minLength: 2, proto: "Array", @@ -55,7 +52,6 @@ contextualize(() => { tContains.internal.firstReferenceOfKindOrThrow( "predicate" ).serializedPredicate - attest(tContains.infer) attest(tContains.json).snap({ proto: "Array", predicate: [predicateRef] @@ -70,7 +66,6 @@ contextualize(() => { type: "array", maxItems: 5 }) - attest(tMaxItems.infer) attest(tMaxItems.json).snap({ proto: "Array", maxLength: 5 @@ -86,7 +81,6 @@ contextualize(() => { type: "array", minItems: 5 }) - attest(tMinItems.infer) attest(tMinItems.json).snap({ proto: "Array", minLength: 5 @@ -106,7 +100,6 @@ contextualize(() => { tUniqueItems.internal.firstReferenceOfKindOrThrow( "predicate" ).serializedPredicate - attest(tUniqueItems.infer) attest(tUniqueItems.json).snap({ proto: "Array", predicate: [predicateRef] diff --git a/ark/jsonschema/__tests__/json.test.ts b/ark/jsonschema/__tests__/json.test.ts index fd54c1170a..3b9bf55ebc 100644 --- a/ark/jsonschema/__tests__/json.test.ts +++ b/ark/jsonschema/__tests__/json.test.ts @@ -1,12 +1,10 @@ import { attest, contextualize } from "@ark/attest" import { parseJsonSchema } from "@ark/jsonschema" -import type { applyConstraintSchema, number } from "arktype" contextualize(() => { it("array", () => { // unknown[] const parsedJsonSchemaArray = parseJsonSchema({ type: "array" } as const) - attest(parsedJsonSchemaArray.infer) attest(parsedJsonSchemaArray.json).snap({ proto: "Array" }) // number[] @@ -14,19 +12,16 @@ contextualize(() => { type: "array", items: { type: "number", minimum: 3 } } as const) - attest(parsedJsonSchemaArrayVariadic.infer) attest(parsedJsonSchemaArrayVariadic.json).snap({ proto: "Array", sequence: { domain: "number", min: 3 } }) - attest[]>(parsedJsonSchemaArrayVariadic.inferBrandableOut) // [string] const parsedJsonSchemaArrayFixed = parseJsonSchema({ type: "array", items: [{ type: "string" }] } as const) - attest<[string]>(parsedJsonSchemaArrayFixed.infer) attest(parsedJsonSchemaArrayFixed.json).snap({ exactLength: 1, proto: "Array", @@ -39,7 +34,6 @@ contextualize(() => { items: [{ type: "string" }], additionalItems: { type: "number" } } as const) - attest<[string, ...number[]]>(parsedJsonSchemaArrayFixedWithVariadic.infer) // Maximum Length const parsedJsonSchemaArrayMaxLength = parseJsonSchema({ @@ -47,10 +41,6 @@ contextualize(() => { items: { type: "string" }, maxItems: 5 } as const) - attest(parsedJsonSchemaArrayMaxLength.infer) - attest>( - parsedJsonSchemaArrayMaxLength.tOut - ) // Minimum Length const parsedJsonSchemaArrayMinLength = parseJsonSchema({ @@ -58,10 +48,6 @@ contextualize(() => { items: { type: "number" }, minItems: 3 } as const) - attest(parsedJsonSchemaArrayMinLength.infer) - attest>( - parsedJsonSchemaArrayMinLength.tOut - ) // Maximum & Minimum Length const parsedJsonSchemaArrayMaxAndMinLength = parseJsonSchema({ @@ -70,14 +56,6 @@ contextualize(() => { maxItems: 5, minItems: 3 } as const) - attest(parsedJsonSchemaArrayMaxAndMinLength.infer) - attest< - applyConstraintSchema< - applyConstraintSchema, - "minLength", - 3 - > - >(parsedJsonSchemaArrayMaxAndMinLength.tOut) }) it("number", () => {}) diff --git a/ark/jsonschema/__tests__/number.test.ts b/ark/jsonschema/__tests__/number.test.ts index 90ac504e56..8d3b64a2f7 100644 --- a/ark/jsonschema/__tests__/number.test.ts +++ b/ark/jsonschema/__tests__/number.test.ts @@ -9,13 +9,11 @@ contextualize(() => { const expectedArkTypeSchema = { domain: "number" } as const const parsedNumberValidator = parseJsonSchema(jsonSchema) - attest(parsedNumberValidator.infer) attest(parsedNumberValidator.json).snap(expectedArkTypeSchema) }) it("type integer", () => { const t = parseJsonSchema({ type: "integer" }) - attest(t.infer) attest(t.json).snap({ domain: "number", divisor: 1 }) }) @@ -24,7 +22,6 @@ contextualize(() => { type: "number", maximum: 5 }) - attest(tMax.infer) attest(tMax.json).snap({ domain: "number", max: 5 @@ -34,7 +31,6 @@ contextualize(() => { type: "number", exclusiveMaximum: 5 }) - attest(tExclMax.infer) attest(tExclMax.json).snap({ domain: "number", max: { rule: 5, exclusive: true } @@ -53,14 +49,12 @@ contextualize(() => { it("minimum & exclusiveMinimum", () => { const tMin = parseJsonSchema({ type: "number", minimum: 5 }) - attest(tMin.infer) attest(tMin.json).snap({ domain: "number", min: 5 }) const tExclMin = parseJsonSchema({ type: "number", exclusiveMinimum: 5 }) - attest(tExclMin.infer) attest(tExclMin.json).snap({ domain: "number", min: { rule: 5, exclusive: true } @@ -79,14 +73,12 @@ contextualize(() => { it("multipleOf", () => { const t = parseJsonSchema({ type: "number", multipleOf: 5 }) - attest(t.infer) attest(t.json).snap({ domain: "number", divisor: 5 }) const tInt = parseJsonSchema({ type: "integer", multipleOf: 5 }) - attest(tInt.infer) attest(tInt.json).snap({ domain: "number", divisor: 5 }) // JSON Schema allows decimal multipleOf, but ArkType doesn't. diff --git a/ark/jsonschema/__tests__/object.test.ts b/ark/jsonschema/__tests__/object.test.ts index 00c155e719..ff20b7f171 100644 --- a/ark/jsonschema/__tests__/object.test.ts +++ b/ark/jsonschema/__tests__/object.test.ts @@ -6,7 +6,6 @@ import { parseJsonSchema } from "@ark/jsonschema" contextualize(() => { it("type object", () => { const t = parseJsonSchema({ type: "object" }) - attest<{ [x: string]: unknown }>(t.infer) attest(t.json).snap({ domain: "object" }) }) @@ -15,7 +14,6 @@ contextualize(() => { type: "object", maxProperties: 1 }) - attest(tMaxProperties.infer) attest(tMaxProperties.json).snap({ domain: "object" }) attest(tMaxProperties.allows({})).equals(true) attest(tMaxProperties.allows({ foo: 1 })).equals(true) @@ -28,7 +26,6 @@ contextualize(() => { type: "object", minProperties: 2 }) - attest(tMinProperties.infer) attest(tMinProperties.json).snap({ domain: "object" }) attest(tMinProperties.allows({})).equals(false) attest(tMinProperties.allows({ foo: 1 })).equals(false) @@ -45,7 +42,6 @@ contextualize(() => { }, required: ["foo"] }) - attest<{ [x: string]: unknown; foo: string; bar?: number }>(tRequired.infer) attest(tRequired.json).snap({ domain: "object", required: [{ key: "foo", value: "string" }], @@ -78,7 +74,6 @@ contextualize(() => { type: "object", additionalProperties: { type: "number" } }) - attest<{ [x: string]: unknown }>(tAdditionalProperties.infer) attest(tAdditionalProperties.json).snap({ domain: "object", additional: "number" @@ -96,7 +91,6 @@ contextualize(() => { "^[a-z]+$": { type: "string" } } }) - attest<{ [x: string]: unknown }>(tPatternProperties.infer) attest(tPatternProperties.json).snap({ domain: "object", pattern: [{ key: "^[a-z]+$", value: "string" }] diff --git a/ark/jsonschema/__tests__/string.test.ts b/ark/jsonschema/__tests__/string.test.ts index a28ebedfb1..a0dca4ef79 100644 --- a/ark/jsonschema/__tests__/string.test.ts +++ b/ark/jsonschema/__tests__/string.test.ts @@ -7,7 +7,6 @@ import { parseJsonSchema } from "@ark/jsonschema" contextualize(() => { it("type string", () => { const t = parseJsonSchema({ type: "string" }) - attest(t.infer) attest(t.json).snap({ domain: "string" }) }) @@ -16,7 +15,6 @@ contextualize(() => { type: "string", maxLength: 5 }) - attest(tMaxLength.infer) attest(tMaxLength.json).snap({ domain: "string", maxLength: 5 @@ -28,7 +26,6 @@ contextualize(() => { type: "string", minLength: 5 }) - attest(tMinLength.infer) attest(tMinLength.json).snap({ domain: "string", minLength: 5 @@ -40,7 +37,6 @@ contextualize(() => { type: "string", pattern: "es" }) - attest(tPatternString.infer) attest(tPatternString.json).snap({ domain: "string", regex: ["es"] @@ -53,7 +49,6 @@ contextualize(() => { type: "string", pattern: /es/ }) - attest(tPatternRegExp.infer) attest(tPatternRegExp.json).snap({ domain: "string", regex: ["es"] // strips the outer slashes diff --git a/ark/jsonschema/array.ts b/ark/jsonschema/array.ts index c0e752de7f..0912f259ff 100644 --- a/ark/jsonschema/array.ts +++ b/ark/jsonschema/array.ts @@ -4,10 +4,10 @@ import { type Predicate, type TraversalContext } from "@ark/schema" -import { printable, type array } from "@ark/util" -import type { Type, applyConstraintSchema } from "arktype" +import { printable } from "@ark/util" +import type { Type } from "arktype" -import { innerParseJsonSchema, type inferJsonSchema } from "./json.ts" +import { innerParseJsonSchema } from "./json.ts" import { JsonSchema } from "./scope.ts" const deepNormalize = (data: unknown): unknown => @@ -106,54 +106,3 @@ export const validateJsonSchemaArray = JsonSchema.ArraySchema.pipe( return rootSchema(arktypeArraySchema) as never } ) - -type inferArrayOfJsonSchema> = { - [index in keyof tuple]: inferJsonSchema -} - -export type inferJsonSchemaArray = - "additionalItems" extends keyof arraySchema ? - "items" extends keyof arraySchema ? - arraySchema["items"] extends array ? - inferJsonSchemaArrayConstraints< - Omit, - // @ts-ignore - TypeScript complains that this is "excessively deep", despite it correctly resolving the type - [ - ...inferJsonSchemaArrayItems, - ...inferJsonSchema[] - ] - > - : // JSON Schema spec explicitly says that additionalItems MUST be ignored if items is not an array, and it's NOT an error - inferJsonSchemaArray, T> - : inferJsonSchema - : "items" extends keyof arraySchema ? - inferJsonSchemaArray< - Omit, - T & inferJsonSchemaArrayItems - > - : inferJsonSchemaArrayConstraints - -type inferJsonSchemaArrayConstraints = - "maxItems" extends keyof arraySchema ? - inferJsonSchemaArrayConstraints< - Omit, - applyConstraintSchema - > - : "minItems" extends keyof arraySchema ? - inferJsonSchemaArrayConstraints< - Omit, - applyConstraintSchema - > - : T extends {} ? T - : never - -type inferJsonSchemaArrayItems = - arrayItemsSchema extends array ? - arrayItemsSchema["length"] extends 0 ? - // JSON Schema explicitly states that {items: []} means "an array of anything" - // https://json-schema.org/understanding-json-schema/reference/array#items - unknown[] - : arrayItemsSchema extends array ? - inferArrayOfJsonSchema - : never - : inferJsonSchema[] diff --git a/ark/jsonschema/composition.ts b/ark/jsonschema/composition.ts index 94d935d253..abc8c9f48b 100644 --- a/ark/jsonschema/composition.ts +++ b/ark/jsonschema/composition.ts @@ -1,6 +1,5 @@ -import type { array } from "@ark/util" import { type, type Type } from "arktype" -import { innerParseJsonSchema, type inferJsonSchema } from "./json.ts" +import { innerParseJsonSchema } from "./json.ts" import type { JsonSchema } from "./scope.ts" const validateAllOfJsonSchemas = ( @@ -67,29 +66,3 @@ export const parseJsonSchemaCompositionKeywords = ( if ("not" in jsonSchema) return validateNotJsonSchema(jsonSchema.not) if ("oneOf" in jsonSchema) return validateOneOfJsonSchemas(jsonSchema.oneOf) } - -// NB: For simplicity sake, the type level treats 'anyOf' and 'oneOf' as the same. -type inferJsonSchemaAnyOrOneOf = - compositionSchemaValue extends never[] ? - never // is an empty array, so is invalid - : compositionSchemaValue extends array ? - t & inferJsonSchema - : never // is not an array, so is invalid - -export type inferJsonSchemaComposition = - "allOf" extends keyof schema ? - t extends never ? - t // "allOf" has incompatible schemas, so don't keep looking - : schema["allOf"] extends [infer firstSchema, ...infer restOfSchemas] ? - inferJsonSchemaComposition< - { allOf: restOfSchemas }, - inferJsonSchema - > - : schema["allOf"] extends never[] ? - t // have finished inferring schemas - : never // "allOf" isn't an array, so is invalid - : "oneOf" extends keyof schema ? inferJsonSchemaAnyOrOneOf - : "anyOf" extends keyof schema ? inferJsonSchemaAnyOrOneOf - : "not" extends keyof schema ? - t // NB: TypeScript doesn't have "not" types, so can't accurately represent. - : unknown diff --git a/ark/jsonschema/json.ts b/ark/jsonschema/json.ts index 2b1a64d1e4..e5e40d178e 100644 --- a/ark/jsonschema/json.ts +++ b/ark/jsonschema/json.ts @@ -1,67 +1,12 @@ -import { - printable, - throwParseError, - type array, - type ErrorMessage -} from "@ark/util" +import { printable, throwParseError } from "@ark/util" import { type, type Out, type Type } from "arktype" import { parseJsonSchemaAnyKeywords } from "./any.ts" -import { validateJsonSchemaArray, type inferJsonSchemaArray } from "./array.ts" -import { - parseJsonSchemaCompositionKeywords, - type inferJsonSchemaComposition -} from "./composition.ts" -import { - validateJsonSchemaNumber, - type inferJsonSchemaNumber -} from "./number.ts" -import { - validateJsonSchemaObject, - type inferJsonSchemaObject -} from "./object.ts" +import { validateJsonSchemaArray } from "./array.ts" +import { parseJsonSchemaCompositionKeywords } from "./composition.ts" +import { validateJsonSchemaNumber } from "./number.ts" +import { validateJsonSchemaObject } from "./object.ts" import { JsonSchema } from "./scope.ts" -import { - validateJsonSchemaString, - type inferJsonSchemaString -} from "./string.ts" - -type JsonSchemaConstraintKind = "const" | "enum" -type JsonSchemaConst = { const: t } -type JsonSchemaEnum = { enum: readonly t[] } - -type inferJsonSchemaConstraint< - schema, - t, - kind extends JsonSchemaConstraintKind -> = t extends never ? never : t & inferJsonSchema> - -type inferJsonSchemaTypeNoKeywords< - schema extends JsonSchema.TypeWithNoKeywords, - t -> = - schema["type"] extends "boolean" ? t & boolean - : schema["type"] extends "null" ? t & null - : never - -export type inferJsonSchema = - schema extends true ? JsonSchema.Json - : schema extends false ? never - : schema extends Record ? JsonSchema.Json - : schema extends array ? inferJsonSchema - : schema extends JsonSchema.CompositionKeywords ? - inferJsonSchemaComposition - : schema extends JsonSchemaConst ? - inferJsonSchemaConstraint - : schema extends JsonSchemaEnum ? - inferJsonSchemaConstraint - : schema extends JsonSchema.TypeWithNoKeywords ? - inferJsonSchemaTypeNoKeywords - : schema extends JsonSchema.ArraySchema ? inferJsonSchemaArray - : schema extends JsonSchema.NumberSchema ? t & inferJsonSchemaNumber - : schema extends JsonSchema.ObjectSchema ? t & inferJsonSchemaObject - : schema extends JsonSchema.StringSchema ? t & inferJsonSchemaString - : t extends {} ? t - : ErrorMessage<"Failed to infer JSON Schema"> +import { validateJsonSchemaString } from "./string.ts" export const innerParseJsonSchema: Type< (In: JsonSchema.Schema) => Out> @@ -128,6 +73,5 @@ export const innerParseJsonSchema: Type< } ) -export const parseJsonSchema = ( - jsonSchema: t -): Type> => innerParseJsonSchema.assert(jsonSchema) as never +export const parseJsonSchema = (jsonSchema: JsonSchema.Schema): Type => + innerParseJsonSchema.assert(jsonSchema) as never diff --git a/ark/jsonschema/number.ts b/ark/jsonschema/number.ts index d59b531e3a..7ada5c0836 100644 --- a/ark/jsonschema/number.ts +++ b/ark/jsonschema/number.ts @@ -1,6 +1,6 @@ import { rootSchema, type Intersection } from "@ark/schema" import { throwParseError } from "@ark/util" -import type { Type, number } from "arktype" +import type { Type } from "arktype" import { JsonSchema } from "./scope.ts" export const validateJsonSchemaNumber = JsonSchema.NumberSchema.pipe( @@ -44,38 +44,3 @@ export const validateJsonSchemaNumber = JsonSchema.NumberSchema.pipe( return rootSchema(arktypeNumberSchema) as never } ) - -export type inferJsonSchemaNumber = - "exclusiveMaximum" extends keyof numberSchema ? - inferJsonSchemaNumber< - Omit, - T & number.lessThan - > - : "exclusiveMinimum" extends keyof numberSchema ? - inferJsonSchemaNumber< - Omit, - T & number.moreThan - > - : "maximum" extends keyof numberSchema ? - inferJsonSchemaNumber< - Omit, - T & number.atMost - > - : "minimum" extends keyof numberSchema ? - inferJsonSchemaNumber< - Omit, - T & number.atLeast - > - : "multipleOf" extends keyof numberSchema ? - inferJsonSchemaNumber< - Omit & { type: "number" }, - T & number.divisibleBy - > - : "type" extends keyof numberSchema ? - numberSchema["type"] extends "integer" ? - inferJsonSchemaNumber< - Omit & { type: "number" }, - T & number.divisibleBy<1> - > - : T - : never // TODO: Throw type error (must have {type: "number"|"integer"} ) diff --git a/ark/jsonschema/object.ts b/ark/jsonschema/object.ts index f30a8524e8..01ccbfda4a 100644 --- a/ark/jsonschema/object.ts +++ b/ark/jsonschema/object.ts @@ -5,10 +5,10 @@ import { type Predicate, type TraversalContext } from "@ark/schema" -import { conflatenateAll, printable, type show } from "@ark/util" +import { conflatenateAll, printable } from "@ark/util" import type { Type } from "arktype" -import { innerParseJsonSchema, type inferJsonSchema } from "./json.ts" +import { innerParseJsonSchema } from "./json.ts" import { JsonSchema } from "./scope.ts" const parseMinMaxProperties = ( @@ -269,68 +269,3 @@ export const validateJsonSchemaObject = JsonSchema.ObjectSchema.pipe( ) as never } ) - -type inferAdditionalProperties = - objectSchema["additionalProperties" & keyof objectSchema] extends ( - JsonSchema.Schema - ) ? - objectSchema["additionalProperties" & keyof objectSchema] extends false ? - // false means no additional properties are allowed, - // which is the default in TypeScript so just return the current type. - unknown - : { - // It's not possible in TS to accurately infer additional properties - // so we use `unknown` to at least allow unspecified properties. - [key: string]: unknown - } - : never // TODO: Throw type error - -type inferRequiredProperties = { - [P in (objectSchema["required" & keyof objectSchema] & - string[])[number]]: P extends ( - keyof objectSchema["properties" & keyof objectSchema] - ) ? - objectSchema["properties" & keyof objectSchema][P] extends ( - JsonSchema.Schema - ) ? - inferJsonSchema - : never // TODO: Throw type error - : never // TODO: Throw type error -} - -type inferOptionalProperties = { - [P in keyof objectSchema["properties" & - keyof objectSchema]]?: objectSchema["properties" & - keyof objectSchema][P] extends JsonSchema.Schema ? - inferJsonSchema - : never // TODO: Throw type error -} - -// NB: We don't infer `patternProperties` or 'patternProperties' since regex index signatures are not supported in TS -export type inferJsonSchemaObject = - "properties" extends keyof objectSchema ? - "required" extends keyof objectSchema ? - inferJsonSchemaObject< - Omit & { - properties: Omit< - // Remove the required keys - objectSchema["properties"], - (objectSchema["required"] & string[])[number] - > - }, - inferRequiredProperties - > - : // 'required' isn't present, so all properties are optional - inferJsonSchemaObject< - Omit, - inferOptionalProperties extends ( - Record - ) ? - T - : T & inferOptionalProperties - > - : "additionalProperties" extends keyof objectSchema ? - show> - : // additionalProperties isn't present in the schema, which JSON Schema explicitly - // states means extra properties are allowed, so update types accordingly. - show diff --git a/ark/jsonschema/string.ts b/ark/jsonschema/string.ts index e35b147dcb..088bd311d6 100644 --- a/ark/jsonschema/string.ts +++ b/ark/jsonschema/string.ts @@ -1,5 +1,5 @@ import { rootSchema, type Intersection } from "@ark/schema" -import type { Type, string } from "arktype" +import type { Type } from "arktype" import { JsonSchema } from "./scope.ts" export const validateJsonSchemaString = JsonSchema.StringSchema.pipe( @@ -23,21 +23,3 @@ export const validateJsonSchemaString = JsonSchema.StringSchema.pipe( return rootSchema(arktypeStringSchema) as never } ) - -export type inferJsonSchemaString = - "maxLength" extends keyof stringSchema ? - inferJsonSchemaString< - Omit, - T & string.atMostLength - > - : "minLength" extends keyof stringSchema ? - inferJsonSchemaString< - Omit, - T & string.atLeastLength - > - : "pattern" extends keyof stringSchema ? - inferJsonSchemaString< - Omit, - T & string.matching - > - : T From a1f2911ea9fedae706e36b95ac67daa5c19445f9 Mon Sep 17 00:00:00 2001 From: TizzySaurus Date: Sat, 26 Oct 2024 00:05:22 +0100 Subject: [PATCH 47/61] Fix string tests --- ark/jsonschema/__tests__/string.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ark/jsonschema/__tests__/string.test.ts b/ark/jsonschema/__tests__/string.test.ts index a0dca4ef79..00e00ac9d2 100644 --- a/ark/jsonschema/__tests__/string.test.ts +++ b/ark/jsonschema/__tests__/string.test.ts @@ -39,7 +39,7 @@ contextualize(() => { }) attest(tPatternString.json).snap({ domain: "string", - regex: ["es"] + pattern: ["es"] }) // JSON Schema explicitly specifies that regexes MUST NOT be implicitly anchored // https://json-schema.org/draft-07/draft-handrews-json-schema-validation-01#rfc.section.4.3 @@ -51,7 +51,7 @@ contextualize(() => { }) attest(tPatternRegExp.json).snap({ domain: "string", - regex: ["es"] // strips the outer slashes + pattern: ["es"] // strips the outer slashes }) attest(tPatternRegExp.allows("expression")).equals(true) }) From 0e1dac7f17b87bb8a35417a9e4dbd4249c06db1a Mon Sep 17 00:00:00 2001 From: TizzySaurus Date: Sat, 26 Oct 2024 00:05:46 +0100 Subject: [PATCH 48/61] Remove redundant duplicate tests --- ark/jsonschema/__tests__/json.test.ts | 66 --------------------------- 1 file changed, 66 deletions(-) delete mode 100644 ark/jsonschema/__tests__/json.test.ts diff --git a/ark/jsonschema/__tests__/json.test.ts b/ark/jsonschema/__tests__/json.test.ts deleted file mode 100644 index 3b9bf55ebc..0000000000 --- a/ark/jsonschema/__tests__/json.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { attest, contextualize } from "@ark/attest" -import { parseJsonSchema } from "@ark/jsonschema" - -contextualize(() => { - it("array", () => { - // unknown[] - const parsedJsonSchemaArray = parseJsonSchema({ type: "array" } as const) - attest(parsedJsonSchemaArray.json).snap({ proto: "Array" }) - - // number[] - const parsedJsonSchemaArrayVariadic = parseJsonSchema({ - type: "array", - items: { type: "number", minimum: 3 } - } as const) - attest(parsedJsonSchemaArrayVariadic.json).snap({ - proto: "Array", - sequence: { domain: "number", min: 3 } - }) - - // [string] - const parsedJsonSchemaArrayFixed = parseJsonSchema({ - type: "array", - items: [{ type: "string" }] - } as const) - attest(parsedJsonSchemaArrayFixed.json).snap({ - exactLength: 1, - proto: "Array", - sequence: { prefix: ["string"] } - }) - - // [string, ...number[]] - const parsedJsonSchemaArrayFixedWithVariadic = parseJsonSchema({ - type: "array", - items: [{ type: "string" }], - additionalItems: { type: "number" } - } as const) - - // Maximum Length - const parsedJsonSchemaArrayMaxLength = parseJsonSchema({ - type: "array", - items: { type: "string" }, - maxItems: 5 - } as const) - - // Minimum Length - const parsedJsonSchemaArrayMinLength = parseJsonSchema({ - type: "array", - items: { type: "number" }, - minItems: 3 - } as const) - - // Maximum & Minimum Length - const parsedJsonSchemaArrayMaxAndMinLength = parseJsonSchema({ - type: "array", - items: { type: "array", items: { type: "string" } }, - maxItems: 5, - minItems: 3 - } as const) - }) - - it("number", () => {}) - - it("object", () => {}) - - it("string", () => {}) -}) From fc67ff708189c2e1bb6e00398a04cf3b18fc813e Mon Sep 17 00:00:00 2001 From: TizzySaurus Date: Sat, 26 Oct 2024 00:09:31 +0100 Subject: [PATCH 49/61] Fix broken types --- ark/jsonschema/array.ts | 88 +++++++++++++------------- ark/jsonschema/composition.ts | 10 +-- ark/jsonschema/json.ts | 112 +++++++++++++++++----------------- ark/jsonschema/number.ts | 73 +++++++++++----------- ark/jsonschema/object.ts | 81 ++++++++++++------------ ark/jsonschema/scope.ts | 83 ++++++++++++++++++++++++- ark/jsonschema/string.ts | 41 +++++++------ 7 files changed, 280 insertions(+), 208 deletions(-) diff --git a/ark/jsonschema/array.ts b/ark/jsonschema/array.ts index 0912f259ff..8d377b5623 100644 --- a/ark/jsonschema/array.ts +++ b/ark/jsonschema/array.ts @@ -5,9 +5,9 @@ import { type TraversalContext } from "@ark/schema" import { printable } from "@ark/util" -import type { Type } from "arktype" +import type { Out, Type } from "arktype" -import { innerParseJsonSchema } from "./json.ts" +import { parseJsonSchema } from "./json.ts" import { JsonSchema } from "./scope.ts" const deepNormalize = (data: unknown): unknown => @@ -51,58 +51,54 @@ const arrayContainsItemMatchingSchema = ( "an array containing at least one item matching 'contains' schema" ) -export const validateJsonSchemaArray = JsonSchema.ArraySchema.pipe( - (jsonSchema): Type => { - const arktypeArraySchema: Intersection.Schema> = { - proto: "Array" - } +export const validateJsonSchemaArray: Type< + (In: JsonSchema.ArraySchema) => Out>, + any +> = JsonSchema.ArraySchema.pipe(jsonSchema => { + const arktypeArraySchema: Intersection.Schema> = { + proto: "Array" + } - if ("items" in jsonSchema) { - if (Array.isArray(jsonSchema.items)) { - arktypeArraySchema.sequence = { - prefix: jsonSchema.items.map( - item => innerParseJsonSchema.assert(item).internal - ) - } + if ("items" in jsonSchema) { + if (Array.isArray(jsonSchema.items)) { + arktypeArraySchema.sequence = { + prefix: jsonSchema.items.map(item => parseJsonSchema(item).internal) + } - if ("additionalItems" in jsonSchema) { - if (jsonSchema.additionalItems === false) - arktypeArraySchema.exactLength = jsonSchema.items.length - else { - arktypeArraySchema.sequence = { - ...arktypeArraySchema.sequence, - variadic: innerParseJsonSchema.assert(jsonSchema.additionalItems) - .internal - } + if ("additionalItems" in jsonSchema) { + if (jsonSchema.additionalItems === false) + arktypeArraySchema.exactLength = jsonSchema.items.length + else { + arktypeArraySchema.sequence = { + ...arktypeArraySchema.sequence, + variadic: parseJsonSchema(jsonSchema.additionalItems).internal } } - } else { - arktypeArraySchema.sequence = { - variadic: innerParseJsonSchema.assert(jsonSchema.items).internal - } + } + } else { + arktypeArraySchema.sequence = { + variadic: parseJsonSchema(jsonSchema.items).json } } + } - if ("maxItems" in jsonSchema) - arktypeArraySchema.maxLength = jsonSchema.maxItems - if ("minItems" in jsonSchema) - arktypeArraySchema.minLength = jsonSchema.minItems + if ("maxItems" in jsonSchema) + arktypeArraySchema.maxLength = jsonSchema.maxItems + if ("minItems" in jsonSchema) + arktypeArraySchema.minLength = jsonSchema.minItems - const predicates: Predicate.Schema[] = [] - if ("uniqueItems" in jsonSchema && jsonSchema.uniqueItems === true) - predicates.push((arr: unknown[], ctx) => arrayItemsAreUnique(arr, ctx)) + const predicates: Predicate.Schema[] = [] + if ("uniqueItems" in jsonSchema && jsonSchema.uniqueItems === true) + predicates.push((arr: unknown[], ctx) => arrayItemsAreUnique(arr, ctx)) - if ("contains" in jsonSchema) { - const parsedContainsJsonSchema = innerParseJsonSchema.assert( - jsonSchema.contains - ) - predicates.push((arr: unknown[], ctx) => - arrayContainsItemMatchingSchema(arr, parsedContainsJsonSchema, ctx) - ) - } + if ("contains" in jsonSchema) { + const parsedContainsJsonSchema = parseJsonSchema(jsonSchema.contains) + predicates.push((arr: unknown[], ctx) => + arrayContainsItemMatchingSchema(arr, parsedContainsJsonSchema, ctx) + ) + } - arktypeArraySchema.predicate = predicates + arktypeArraySchema.predicate = predicates - return rootSchema(arktypeArraySchema) as never - } -) + return rootSchema(arktypeArraySchema) as never +}) diff --git a/ark/jsonschema/composition.ts b/ark/jsonschema/composition.ts index abc8c9f48b..b47b4baa56 100644 --- a/ark/jsonschema/composition.ts +++ b/ark/jsonschema/composition.ts @@ -1,23 +1,23 @@ import { type, type Type } from "arktype" -import { innerParseJsonSchema } from "./json.ts" +import { parseJsonSchema } from "./json.ts" import type { JsonSchema } from "./scope.ts" const validateAllOfJsonSchemas = ( jsonSchemas: JsonSchema.Schema[] ): Type => jsonSchemas - .map(jsonSchema => innerParseJsonSchema.assert(jsonSchema)) + .map(jsonSchema => parseJsonSchema(jsonSchema)) .reduce((acc, validator) => acc.and(validator)) const validateAnyOfJsonSchemas = ( jsonSchemas: JsonSchema.Schema[] ): Type => jsonSchemas - .map(jsonSchema => innerParseJsonSchema.assert(jsonSchema)) + .map(jsonSchema => parseJsonSchema(jsonSchema)) .reduce((acc, validator) => acc.or(validator)) const validateNotJsonSchema = (jsonSchema: JsonSchema.Schema) => { - const inner = innerParseJsonSchema.assert(jsonSchema) + const inner = parseJsonSchema(jsonSchema) return type("unknown").narrow((data, ctx) => inner.allows(data) ? ctx.mustBe(`not ${inner.description}`) : true ) as Type @@ -25,7 +25,7 @@ const validateNotJsonSchema = (jsonSchema: JsonSchema.Schema) => { const validateOneOfJsonSchemas = (jsonSchemas: JsonSchema.Schema[]) => { const oneOfValidators = jsonSchemas.map(nestedSchema => - innerParseJsonSchema.assert(nestedSchema) + parseJsonSchema(nestedSchema) ) const oneOfValidatorsDescriptions = oneOfValidators.map( validator => `○ ${validator.description}` diff --git a/ark/jsonschema/json.ts b/ark/jsonschema/json.ts index e5e40d178e..4a3c310831 100644 --- a/ark/jsonschema/json.ts +++ b/ark/jsonschema/json.ts @@ -9,69 +9,69 @@ import { JsonSchema } from "./scope.ts" import { validateJsonSchemaString } from "./string.ts" export const innerParseJsonSchema: Type< - (In: JsonSchema.Schema) => Out> -> = JsonSchema.Schema.pipe( - (jsonSchema: JsonSchema.Schema): Type => { - if (typeof jsonSchema === "boolean") { - if (jsonSchema) return JsonSchema.Json - else return type("never") // No runtime value ever passes validation for JSON schema of 'false' - } + (In: JsonSchema.Schema) => Out>, + any +> = JsonSchema.Schema.pipe(jsonSchema => { + if (typeof jsonSchema === "boolean") { + if (jsonSchema) return JsonSchema.Json + else return type("never") // No runtime value ever passes validation for JSON schema of 'false' + } - if (Array.isArray(jsonSchema)) { - return ( - parseJsonSchemaCompositionKeywords({ anyOf: jsonSchema }) ?? - throwParseError( - "Failed to convert root array of JSON Schemas to an anyOf schema" - ) + if (Array.isArray(jsonSchema)) { + return ( + parseJsonSchemaCompositionKeywords({ anyOf: jsonSchema }) ?? + throwParseError( + "Failed to convert root array of JSON Schemas to an anyOf schema" ) - } + ) + } - const constAndOrEnumValidator = parseJsonSchemaAnyKeywords(jsonSchema) - const compositionValidator = parseJsonSchemaCompositionKeywords(jsonSchema) + const constAndOrEnumValidator = parseJsonSchemaAnyKeywords(jsonSchema) + const compositionValidator = parseJsonSchemaCompositionKeywords(jsonSchema) - const preTypeValidator: Type | undefined = - constAndOrEnumValidator ? - compositionValidator ? compositionValidator.and(constAndOrEnumValidator) - : constAndOrEnumValidator - : compositionValidator + const preTypeValidator: Type | undefined = + constAndOrEnumValidator ? + compositionValidator ? compositionValidator.and(constAndOrEnumValidator) + : constAndOrEnumValidator + : compositionValidator - if ("type" in jsonSchema) { - let typeValidator: Type - switch (jsonSchema.type) { - case "array": - typeValidator = validateJsonSchemaArray.assert(jsonSchema) - break - case "boolean": - case "null": - typeValidator = type(jsonSchema.type) - break - case "integer": - case "number": - typeValidator = validateJsonSchemaNumber.assert(jsonSchema) - break - case "object": - typeValidator = validateJsonSchemaObject.assert(jsonSchema) - break - case "string": - typeValidator = validateJsonSchemaString.assert(jsonSchema) - break - default: - throwParseError( - // @ts-expect-error -- All valid 'type' values should be handled above - `Provided 'type' value must be a supported JSON Schema type (was '${jsonSchema.type}')` - ) - } - if (preTypeValidator === undefined) return typeValidator - return typeValidator.and(preTypeValidator) - } - if (preTypeValidator === undefined) { - throwParseError( - `Provided JSON Schema must have one of 'type', 'enum', 'const', 'allOf', 'anyOf' but was ${printable(jsonSchema)}.` - ) + if ("type" in jsonSchema) { + let typeValidator: Type + + switch (jsonSchema.type) { + case "array": + typeValidator = validateJsonSchemaArray.assert(jsonSchema) as never // A bug in ArkType makes this cast necessary + break + case "boolean": + case "null": + typeValidator = type(jsonSchema.type) + break + case "integer": + case "number": + typeValidator = validateJsonSchemaNumber.assert(jsonSchema) as never // A bug in ArkType makes this cast necessary + break + case "object": + typeValidator = validateJsonSchemaObject.assert(jsonSchema) as never // A bug in ArkType makes this cast necessary + break + case "string": + typeValidator = validateJsonSchemaString.assert(jsonSchema) as never // A bug in ArkType makes this cast necessary + break + default: + throwParseError( + // @ts-expect-error -- All valid 'type' values should be handled above + `Provided 'type' value must be a supported JSON Schema type (was '${jsonSchema.type}')` + ) } - return preTypeValidator + if (preTypeValidator === undefined) return typeValidator + return typeValidator.and(preTypeValidator) + } + if (preTypeValidator === undefined) { + throwParseError( + `Provided JSON Schema must have one of 'type', 'enum', 'const', 'allOf', 'anyOf' but was ${printable(jsonSchema)}.` + ) } -) + return preTypeValidator +}) export const parseJsonSchema = (jsonSchema: JsonSchema.Schema): Type => innerParseJsonSchema.assert(jsonSchema) as never diff --git a/ark/jsonschema/number.ts b/ark/jsonschema/number.ts index 7ada5c0836..733ed4989a 100644 --- a/ark/jsonschema/number.ts +++ b/ark/jsonschema/number.ts @@ -1,46 +1,47 @@ import { rootSchema, type Intersection } from "@ark/schema" import { throwParseError } from "@ark/util" -import type { Type } from "arktype" +import type { Out, Type } from "arktype" import { JsonSchema } from "./scope.ts" -export const validateJsonSchemaNumber = JsonSchema.NumberSchema.pipe( - (jsonSchema): Type => { - const arktypeNumberSchema: Intersection.Schema = { - domain: "number" - } +export const validateJsonSchemaNumber: Type< + (In: JsonSchema.NumberSchema) => Out>, + any +> = JsonSchema.NumberSchema.pipe((jsonSchema): Type => { + const arktypeNumberSchema: Intersection.Schema = { + domain: "number" + } - if ("maximum" in jsonSchema) { - if ("exclusiveMaximum" in jsonSchema) { - throwParseError( - "Provided number JSON Schema cannot have 'maximum' and 'exclusiveMaximum" - ) - } - arktypeNumberSchema.max = jsonSchema.maximum - } else if ("exclusiveMaximum" in jsonSchema) { - arktypeNumberSchema.max = { - rule: jsonSchema.exclusiveMaximum, - exclusive: true - } + if ("maximum" in jsonSchema) { + if ("exclusiveMaximum" in jsonSchema) { + throwParseError( + "Provided number JSON Schema cannot have 'maximum' and 'exclusiveMaximum" + ) } + arktypeNumberSchema.max = jsonSchema.maximum + } else if ("exclusiveMaximum" in jsonSchema) { + arktypeNumberSchema.max = { + rule: jsonSchema.exclusiveMaximum, + exclusive: true + } + } - if ("minimum" in jsonSchema) { - if ("exclusiveMinimum" in jsonSchema) { - throwParseError( - "Provided number JSON Schema cannot have 'minimum' and 'exclusiveMinimum" - ) - } - arktypeNumberSchema.min = jsonSchema.minimum - } else if ("exclusiveMinimum" in jsonSchema) { - arktypeNumberSchema.min = { - rule: jsonSchema.exclusiveMinimum, - exclusive: true - } + if ("minimum" in jsonSchema) { + if ("exclusiveMinimum" in jsonSchema) { + throwParseError( + "Provided number JSON Schema cannot have 'minimum' and 'exclusiveMinimum" + ) } + arktypeNumberSchema.min = jsonSchema.minimum + } else if ("exclusiveMinimum" in jsonSchema) { + arktypeNumberSchema.min = { + rule: jsonSchema.exclusiveMinimum, + exclusive: true + } + } - if ("multipleOf" in jsonSchema) - arktypeNumberSchema.divisor = jsonSchema.multipleOf - else if (jsonSchema.type === "integer") arktypeNumberSchema.divisor = 1 + if ("multipleOf" in jsonSchema) + arktypeNumberSchema.divisor = jsonSchema.multipleOf + else if (jsonSchema.type === "integer") arktypeNumberSchema.divisor = 1 - return rootSchema(arktypeNumberSchema) as never - } -) + return rootSchema(arktypeNumberSchema) as never +}) diff --git a/ark/jsonschema/object.ts b/ark/jsonschema/object.ts index 01ccbfda4a..0a907cfdb8 100644 --- a/ark/jsonschema/object.ts +++ b/ark/jsonschema/object.ts @@ -6,9 +6,9 @@ import { type TraversalContext } from "@ark/schema" import { conflatenateAll, printable } from "@ark/util" -import type { Type } from "arktype" +import type { Out, Type } from "arktype" -import { innerParseJsonSchema } from "./json.ts" +import { parseJsonSchema } from "./json.ts" import { JsonSchema } from "./scope.ts" const parseMinMaxProperties = ( @@ -56,8 +56,7 @@ const parsePatternProperties = ( if (!("patternProperties" in jsonSchema)) return const patternProperties = Object.entries(jsonSchema.patternProperties).map( - ([key, value]) => - [new RegExp(key), innerParseJsonSchema.assert(value)] as const + ([key, value]) => [new RegExp(key), parseJsonSchema(value)] as const ) // Ensure that the schema for any property is compatible with any corresponding patternProperties @@ -66,8 +65,7 @@ const parsePatternProperties = ( ([property, schemaForProperty]) => { if (!pattern.test(property)) return - const parsedPropertySchema = - innerParseJsonSchema.assert(schemaForProperty) + const parsedPropertySchema = parseJsonSchema(schemaForProperty) if (!parsedPropertySchema.overlaps(parsedPatternPropertySchema)) { ctx.reject({ @@ -105,9 +103,7 @@ const parsePropertyNames = ( ) => { if (!("propertyNames" in jsonSchema)) return - const propertyNamesValidator = innerParseJsonSchema.assert( - jsonSchema.propertyNames - ) + const propertyNamesValidator = parseJsonSchema(jsonSchema.propertyNames) if ( "domain" in propertyNamesValidator.json && @@ -178,11 +174,11 @@ const parseRequiredAndOptionalKeys = ( return { optionalKeys: optionalKeys.map(key => ({ key, - value: innerParseJsonSchema.assert(jsonSchema.properties![key]).internal + value: parseJsonSchema(jsonSchema.properties![key]).internal })), requiredKeys: requiredKeys.map(key => ({ key, - value: innerParseJsonSchema.assert(jsonSchema.properties![key]).internal + value: parseJsonSchema(jsonSchema.properties![key]).internal })) } } @@ -217,7 +213,7 @@ const parseAdditionalProperties = (jsonSchema: JsonSchema.ObjectSchema) => { return } - const additionalPropertyValidator = innerParseJsonSchema.assert( + const additionalPropertyValidator = parseJsonSchema( additionalPropertiesSchema ) @@ -234,38 +230,39 @@ const parseAdditionalProperties = (jsonSchema: JsonSchema.ObjectSchema) => { } } -export const validateJsonSchemaObject = JsonSchema.ObjectSchema.pipe( - (jsonSchema, ctx): Type => { - const arktypeObjectSchema: Intersection.Schema = { - domain: "object" - } +export const validateJsonSchemaObject: Type< + (In: JsonSchema.ObjectSchema) => Out>, + any +> = JsonSchema.ObjectSchema.pipe((jsonSchema, ctx): Type => { + const arktypeObjectSchema: Intersection.Schema = { + domain: "object" + } - const { requiredKeys, optionalKeys } = parseRequiredAndOptionalKeys( - jsonSchema, - ctx - ) - arktypeObjectSchema.required = requiredKeys - arktypeObjectSchema.optional = optionalKeys + const { requiredKeys, optionalKeys } = parseRequiredAndOptionalKeys( + jsonSchema, + ctx + ) + arktypeObjectSchema.required = requiredKeys + arktypeObjectSchema.optional = optionalKeys - const predicates = conflatenateAll( - ...parseMinMaxProperties(jsonSchema, ctx), - parsePropertyNames(jsonSchema, ctx), - parsePatternProperties(jsonSchema, ctx), - parseAdditionalProperties(jsonSchema) - ) + const predicates = conflatenateAll( + ...parseMinMaxProperties(jsonSchema, ctx), + parsePropertyNames(jsonSchema, ctx), + parsePatternProperties(jsonSchema, ctx), + parseAdditionalProperties(jsonSchema) + ) - const typeWithoutPredicates = rootSchema(arktypeObjectSchema) - if (predicates.length === 0) return typeWithoutPredicates as never + const typeWithoutPredicates = rootSchema(arktypeObjectSchema) + if (predicates.length === 0) return typeWithoutPredicates as never - return rootSchema({ domain: "object", predicate: predicates }).narrow( - (obj: object, innerCtx) => { - const validationResult = typeWithoutPredicates(obj) - if (validationResult instanceof ArkErrors) { - innerCtx.errors.merge(validationResult) - return false - } - return true + return rootSchema({ domain: "object", predicate: predicates }).narrow( + (obj: object, innerCtx) => { + const validationResult = typeWithoutPredicates(obj) + if (validationResult instanceof ArkErrors) { + innerCtx.errors.merge(validationResult) + return false } - ) as never - } -) + return true + } + ) as never +}) diff --git a/ark/jsonschema/scope.ts b/ark/jsonschema/scope.ts index c8a49ca8e5..183c1d20e6 100644 --- a/ark/jsonschema/scope.ts +++ b/ark/jsonschema/scope.ts @@ -1,6 +1,83 @@ -import { scope } from "arktype" +import { scope, type Scope } from "arktype" -const $ = scope({ +type AnyKeywords = { + const?: unknown + enum?: unknown[] +} +type CompositionKeywords = { + allOf?: Schema[] + anyOf?: Schema[] + oneOf?: Schema[] + not?: Schema +} +type TypeWithNoKeywords = { type: "boolean" | "null" } +type TypeWithKeywords = ArraySchema | NumberSchema | ObjectSchema | StringSchema +// NB: For sake of simplicitly, at runtime it's assumed that +// whatever we're parsing is valid JSON since it will be 99% of the time. +// This decision may be changed later, e.g. when a built-in JSON type exists in AT. +type Json = unknown +type BaseSchema = + // NB: `true` means "accept an valid JSON"; `false` means "reject everything". + | boolean + | TypeWithNoKeywords + | TypeWithKeywords + | AnyKeywords + | CompositionKeywords +type Schema = BaseSchema | BaseSchema[] +type ArraySchema = { + additionalItems?: Schema + contains?: Schema + // JSON Schema states that if 'items' is not present, then treat as an empty schema (i.e. accept any valid JSON) + items?: Schema | Schema[] + maxItems?: number + minItems?: number + type: "array" + uniqueItems?: boolean +} +type NumberSchema = { + // NB: Technically 'exclusiveMaximum' and 'exclusiveMinimum' are mutually exclusive with 'maximum' and 'minimum', respectively, + // which is reflected at runtime but it's not worth the performance cost to validate this statically. + exclusiveMaximum?: number + exclusiveMinimum?: number + maximum?: number + minimum?: number + // NB: JSON Schema allows decimal multipleOf, but ArkType only supports integer. + multipleOf?: number + type: "number" | "integer" +} +type ObjectSchema = { + additionalProperties?: Schema + maxProperties?: number + minProperties?: number + patternProperties?: { [k: string]: Schema } + // NB: Technically 'properties' is required when 'required' is present, + // which is reflected at runtime but it's not worth the performance cost to validate this statically. + properties?: { [k: string]: Schema } + propertyNames?: Schema + required?: string[] + type: "object" +} +type StringSchema = { + maxLength?: number + minLength?: number + pattern?: RegExp | string + type: "string" +} + +type JsonSchemaScope = Scope<{ + AnyKeywords: AnyKeywords + CompositionKeywords: CompositionKeywords + TypeWithNoKeywords: TypeWithNoKeywords + TypeWithKeywords: TypeWithKeywords + Json: Json + Schema: Schema + ArraySchema: ArraySchema + NumberSchema: NumberSchema + ObjectSchema: ObjectSchema + StringSchema: StringSchema +}> + +const $: JsonSchemaScope = scope({ AnyKeywords: { "const?": "unknown", "enum?": "unknown[]" @@ -60,7 +137,7 @@ const $ = scope({ "pattern?": "RegExp | string", type: "'string'" } -}) +}) as unknown as JsonSchemaScope export const JsonSchema = $.export() export declare namespace JsonSchema { diff --git a/ark/jsonschema/string.ts b/ark/jsonschema/string.ts index 088bd311d6..88a928a75b 100644 --- a/ark/jsonschema/string.ts +++ b/ark/jsonschema/string.ts @@ -1,25 +1,26 @@ import { rootSchema, type Intersection } from "@ark/schema" -import type { Type } from "arktype" +import type { Out, Type } from "arktype" import { JsonSchema } from "./scope.ts" -export const validateJsonSchemaString = JsonSchema.StringSchema.pipe( - (jsonSchema): Type => { - const arktypeStringSchema: Intersection.Schema = { - domain: "string" - } +export const validateJsonSchemaString: Type< + (In: JsonSchema.StringSchema) => Out>, + any +> = JsonSchema.StringSchema.pipe((jsonSchema): Type => { + const arktypeStringSchema: Intersection.Schema = { + domain: "string" + } - if ("maxLength" in jsonSchema) - arktypeStringSchema.maxLength = jsonSchema.maxLength - if ("minLength" in jsonSchema) - arktypeStringSchema.minLength = jsonSchema.minLength - if ("pattern" in jsonSchema) { - if (jsonSchema.pattern instanceof RegExp) { - arktypeStringSchema.pattern = [ - // Strip leading and trailing slashes from RegExp - jsonSchema.pattern.toString().slice(1, -1) - ] - } else arktypeStringSchema.pattern = [jsonSchema.pattern] - } - return rootSchema(arktypeStringSchema) as never + if ("maxLength" in jsonSchema) + arktypeStringSchema.maxLength = jsonSchema.maxLength + if ("minLength" in jsonSchema) + arktypeStringSchema.minLength = jsonSchema.minLength + if ("pattern" in jsonSchema) { + if (jsonSchema.pattern instanceof RegExp) { + arktypeStringSchema.pattern = [ + // Strip leading and trailing slashes from RegExp + jsonSchema.pattern.toString().slice(1, -1) + ] + } else arktypeStringSchema.pattern = [jsonSchema.pattern] } -) + return rootSchema(arktypeStringSchema) as never +}) From aa817827942775dd8e336ba9a059e8cb45237dfb Mon Sep 17 00:00:00 2001 From: TizzySaurus Date: Sat, 26 Oct 2024 12:04:51 +0100 Subject: [PATCH 50/61] Use 'expected' and 'actual' props in ctx.reject and use 'Type' over 'Type' --- ark/jsonschema/array.ts | 9 ++++--- ark/jsonschema/composition.ts | 29 +++++++++++--------- ark/jsonschema/json.ts | 2 +- ark/jsonschema/object.ts | 51 +++++++++++++++++++++-------------- 4 files changed, 53 insertions(+), 38 deletions(-) diff --git a/ark/jsonschema/array.ts b/ark/jsonschema/array.ts index 8d377b5623..1a471f3cec 100644 --- a/ark/jsonschema/array.ts +++ b/ark/jsonschema/array.ts @@ -42,14 +42,15 @@ const arrayItemsAreUnique = ( const arrayContainsItemMatchingSchema = ( array: readonly unknown[], - schema: Type, + schema: Type, ctx: TraversalContext ) => array.some(item => schema.allows(item)) === true ? true - : ctx.mustBe( - "an array containing at least one item matching 'contains' schema" - ) + : ctx.reject({ + expected: `an array containing at least one item matching 'contains' schema of ${schema.description}`, + actual: printable(array) + }) export const validateJsonSchemaArray: Type< (In: JsonSchema.ArraySchema) => Out>, diff --git a/ark/jsonschema/composition.ts b/ark/jsonschema/composition.ts index b47b4baa56..be5daf74af 100644 --- a/ark/jsonschema/composition.ts +++ b/ark/jsonschema/composition.ts @@ -1,17 +1,14 @@ +import { printable } from "@ark/util" import { type, type Type } from "arktype" import { parseJsonSchema } from "./json.ts" import type { JsonSchema } from "./scope.ts" -const validateAllOfJsonSchemas = ( - jsonSchemas: JsonSchema.Schema[] -): Type => +const validateAllOfJsonSchemas = (jsonSchemas: JsonSchema.Schema[]): Type => jsonSchemas .map(jsonSchema => parseJsonSchema(jsonSchema)) .reduce((acc, validator) => acc.and(validator)) -const validateAnyOfJsonSchemas = ( - jsonSchemas: JsonSchema.Schema[] -): Type => +const validateAnyOfJsonSchemas = (jsonSchemas: JsonSchema.Schema[]): Type => jsonSchemas .map(jsonSchema => parseJsonSchema(jsonSchema)) .reduce((acc, validator) => acc.or(validator)) @@ -19,8 +16,13 @@ const validateAnyOfJsonSchemas = ( const validateNotJsonSchema = (jsonSchema: JsonSchema.Schema) => { const inner = parseJsonSchema(jsonSchema) return type("unknown").narrow((data, ctx) => - inner.allows(data) ? ctx.mustBe(`not ${inner.description}`) : true - ) as Type + inner.allows(data) ? + ctx.reject({ + expected: `a value that's not ${inner.description}`, + actual: printable(data) + }) + : true + ) as Type } const validateOneOfJsonSchemas = (jsonSchemas: JsonSchema.Schema[]) => { @@ -41,15 +43,16 @@ const validateOneOfJsonSchemas = (jsonSchemas: JsonSchema.Schema[]) => { matchedValidator = validator continue } - return ctx.mustBe( - `exactly one of:\n${oneOfValidatorsDescriptions.join("\n")}` - ) + return ctx.reject({ + expected: `exactly one of:\n${oneOfValidatorsDescriptions.join("\n")}`, + actual: printable(data) + }) } } return matchedValidator !== undefined - }) as Type + }) as Type ) - // TODO: Theoretically this shouldn't be necessary due to above `ctx.mustBe` in narrow??? + // TODO: Theoretically this shouldn't be necessary due to above `ctx.rejects` in narrow??? .describe(`one of:\n${oneOfValidatorsDescriptions.join("\n")}\n`) ) } diff --git a/ark/jsonschema/json.ts b/ark/jsonschema/json.ts index 4a3c310831..4c9d51e437 100644 --- a/ark/jsonschema/json.ts +++ b/ark/jsonschema/json.ts @@ -73,5 +73,5 @@ export const innerParseJsonSchema: Type< return preTypeValidator }) -export const parseJsonSchema = (jsonSchema: JsonSchema.Schema): Type => +export const parseJsonSchema = (jsonSchema: JsonSchema.Schema): Type => innerParseJsonSchema.assert(jsonSchema) as never diff --git a/ark/jsonschema/object.ts b/ark/jsonschema/object.ts index 0a907cfdb8..591a8a60f6 100644 --- a/ark/jsonschema/object.ts +++ b/ark/jsonschema/object.ts @@ -21,7 +21,8 @@ const parseMinMaxProperties = ( if ((jsonSchema.required?.length ?? 0) > maxProperties) { ctx.reject({ - message: `The specified JSON Schema requires at least ${jsonSchema.required?.length} properties, which exceeds the specified maxProperties of ${jsonSchema.maxProperties}.` + expected: `an object JSON Schema with at most ${jsonSchema.maxProperties} required properties`, + actual: `an object JSON Schema with ${jsonSchema.required!.length} required properties` }) } predicates.push((data: object, ctx) => { @@ -29,8 +30,8 @@ const parseMinMaxProperties = ( return keys.length <= maxProperties ? true : ctx.reject({ - expected: `at most ${maxProperties} propert${maxProperties === 1 ? "y" : "ies"}`, - actual: keys.length.toString() + expected: `an object with at most ${maxProperties} propert${maxProperties === 1 ? "y" : "ies"}`, + actual: `an object with ${keys.length.toString()} propert${maxProperties === 1 ? "y" : "ies"}` }) }) } @@ -41,8 +42,8 @@ const parseMinMaxProperties = ( return keys.length >= minProperties ? true : ctx.reject({ - expected: `at least ${minProperties} propert${minProperties === 1 ? "y" : "ies"}`, - actual: keys.length.toString() + expected: `an object with at least ${minProperties} propert${minProperties === 1 ? "y" : "ies"}`, + actual: `an object with ${keys.length.toString()} propert${minProperties === 1 ? "y" : "ies"}` }) }) } @@ -69,7 +70,9 @@ const parsePatternProperties = ( if (!parsedPropertySchema.overlaps(parsedPatternPropertySchema)) { ctx.reject({ - message: `property ${property} must have a schema that overlaps with the patternProperty ${pattern}` + path: [property], + expected: `a JSON Schema that overlaps with the schema for patternProperty ${pattern} (${parsedPatternPropertySchema.description})`, + actual: parsedPropertySchema.description }) } } @@ -77,7 +80,7 @@ const parsePatternProperties = ( }) // NB: We don't validate compatability of schemas for overlapping patternProperties - // since getting the intersection of regexes is inherenetly difficult. + // since getting the intersection of regexes is inherently non-trivial. return (data: object, ctx: TraversalContext) => { const errors: false[] = [] @@ -86,8 +89,9 @@ const parsePatternProperties = ( if (pattern.test(dataKey) && !parsedJsonSchema.allows(dataValue)) { errors.push( ctx.reject({ - actual: dataValue, - expected: `${parsedJsonSchema.description} as property ${dataKey} matches patternProperty ${pattern}` + path: [dataKey], + expected: `${parsedJsonSchema.description}, as property ${dataKey} matches patternProperty ${pattern}`, + actual: printable(dataValue) }) ) } @@ -111,8 +115,8 @@ const parsePropertyNames = ( ) { ctx.reject({ path: ["propertyNames"], - actual: `a schema for validating a ${propertyNamesValidator.json.domain as string}`, - expected: "a schema for validating a string" + expected: "a schema for validating a string", + actual: `a schema for validating a ${printable(propertyNamesValidator.json.domain)}` }) } @@ -123,7 +127,9 @@ const parsePropertyNames = ( if (!propertyNamesValidator.allows(key)) { errors.push( ctx.reject({ - message: `property ${key} doesn't adhere to the propertyNames schema of ${propertyNamesValidator.description}` + path: [key], + expected: `a key adhering to the propertyNames schema of ${propertyNamesValidator.description}`, + actual: key }) ) } @@ -142,8 +148,9 @@ const parseRequiredAndOptionalKeys = ( if ("required" in jsonSchema) { if (jsonSchema.required.length !== new Set(jsonSchema.required).size) { ctx.reject({ + path: ["required"], expected: "an array of unique strings", - path: ["required"] + actual: printable(jsonSchema.required) }) } @@ -151,9 +158,9 @@ const parseRequiredAndOptionalKeys = ( if (key in jsonSchema.properties) requiredKeys.push(key) else { ctx.reject({ - actual: key, - expected: "a key in the 'properties' object", - path: ["required"] + path: ["required"], + expected: `a key from the 'properties' object (one of ${printable(Object.keys(jsonSchema.properties))})`, + actual: key }) } } @@ -165,9 +172,9 @@ const parseRequiredAndOptionalKeys = ( } } else if ("required" in jsonSchema) { ctx.reject({ + expected: "a valid object JSON Schema", actual: - "an object JSON Schema with 'required' array but no 'properties' object", - expected: "a valid object JSON Schema" + "an object JSON Schema with 'required' array but no 'properties' object" }) } @@ -207,7 +214,9 @@ const parseAdditionalProperties = (jsonSchema: JsonSchema.ObjectSchema) => { if (additionalPropertiesSchema === false) { errors.push( ctx.reject({ - message: `property ${key} is an additional property, which the provided schema does not allow` + expected: + "an object with no additional keys, since provided additionalProperties JSON Schema doesn't allow it", + actual: `an additional key (${key})` }) ) return @@ -221,7 +230,9 @@ const parseAdditionalProperties = (jsonSchema: JsonSchema.ObjectSchema) => { if (!additionalPropertyValidator.allows(value)) { errors.push( ctx.reject({ - problem: `property ${key} is an additional property so must adhere to additional property schema of ${additionalPropertyValidator.description} (was ${printable(value)})` + path: [key], + expected: `${additionalPropertyValidator.description}, since ${key} is an additional property.`, + actual: printable(value) }) ) } From de4c7ccfab05f6100779f5e60b37e42a18e664f3 Mon Sep 17 00:00:00 2001 From: TizzySaurus Date: Sat, 26 Oct 2024 12:18:07 +0100 Subject: [PATCH 51/61] Use ctx.hasError instead of manually tracking ctx errors --- ark/jsonschema/object.ts | 60 +++++++++++++++------------------------- 1 file changed, 23 insertions(+), 37 deletions(-) diff --git a/ark/jsonschema/object.ts b/ark/jsonschema/object.ts index 591a8a60f6..1b17974201 100644 --- a/ark/jsonschema/object.ts +++ b/ark/jsonschema/object.ts @@ -82,22 +82,18 @@ const parsePatternProperties = ( // NB: We don't validate compatability of schemas for overlapping patternProperties // since getting the intersection of regexes is inherently non-trivial. return (data: object, ctx: TraversalContext) => { - const errors: false[] = [] - Object.entries(data).forEach(([dataKey, dataValue]) => { patternProperties.forEach(([pattern, parsedJsonSchema]) => { if (pattern.test(dataKey) && !parsedJsonSchema.allows(dataValue)) { - errors.push( - ctx.reject({ - path: [dataKey], - expected: `${parsedJsonSchema.description}, as property ${dataKey} matches patternProperty ${pattern}`, - actual: printable(dataValue) - }) - ) + ctx.reject({ + path: [dataKey], + expected: `${parsedJsonSchema.description}, as property ${dataKey} matches patternProperty ${pattern}`, + actual: printable(dataValue) + }) } }) }) - return errors.length === 0 + return ctx.hasError() } } @@ -121,20 +117,16 @@ const parsePropertyNames = ( } return (data: object, ctx: TraversalContext) => { - const errors: false[] = [] - Object.keys(data).forEach(key => { if (!propertyNamesValidator.allows(key)) { - errors.push( - ctx.reject({ - path: [key], - expected: `a key adhering to the propertyNames schema of ${propertyNamesValidator.description}`, - actual: key - }) - ) + ctx.reject({ + path: [key], + expected: `a key adhering to the propertyNames schema of ${propertyNamesValidator.description}`, + actual: key + }) } }) - return errors.length === 0 + return ctx.hasError() } } @@ -201,8 +193,6 @@ const parseAdditionalProperties = (jsonSchema: JsonSchema.ObjectSchema) => { if (additionalPropertiesSchema === true) return return (data: object, ctx: TraversalContext) => { - const errors: false[] = [] - Object.keys(data).forEach(key => { if ( properties.includes(key) || @@ -212,13 +202,11 @@ const parseAdditionalProperties = (jsonSchema: JsonSchema.ObjectSchema) => { return if (additionalPropertiesSchema === false) { - errors.push( - ctx.reject({ - expected: - "an object with no additional keys, since provided additionalProperties JSON Schema doesn't allow it", - actual: `an additional key (${key})` - }) - ) + ctx.reject({ + expected: + "an object with no additional keys, since provided additionalProperties JSON Schema doesn't allow it", + actual: `an additional key (${key})` + }) return } @@ -228,16 +216,14 @@ const parseAdditionalProperties = (jsonSchema: JsonSchema.ObjectSchema) => { const value = data[key as keyof typeof data] if (!additionalPropertyValidator.allows(value)) { - errors.push( - ctx.reject({ - path: [key], - expected: `${additionalPropertyValidator.description}, since ${key} is an additional property.`, - actual: printable(value) - }) - ) + ctx.reject({ + path: [key], + expected: `${additionalPropertyValidator.description}, since ${key} is an additional property.`, + actual: printable(value) + }) } }) - return errors.length === 0 + return ctx.hasError() } } From ce9b2130040cb81fbe1b8ab99fd4506d30a9e55f Mon Sep 17 00:00:00 2001 From: TizzySaurus Date: Tue, 29 Oct 2024 21:13:30 +0000 Subject: [PATCH 52/61] Fix incorrect exports from ark/type/index.ts --- ark/type/index.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/ark/type/index.ts b/ark/type/index.ts index 5b91c86d1f..05d0ebf355 100644 --- a/ark/type/index.ts +++ b/ark/type/index.ts @@ -6,13 +6,8 @@ export { type JsonSchema } from "@ark/schema" export { Hkt, inferred } from "@ark/util" +export type { Out, number, string } from "./attributes.ts" export { Generic } from "./generic.ts" -export type { - Out, - applyConstraintSchema, - number, - string -} from "./keywords/inference.ts" export { ark, declare, From ad9e05310c5c6e5849590fd0aa0e22d7c8fe5d1e Mon Sep 17 00:00:00 2001 From: TizzySaurus Date: Tue, 29 Oct 2024 21:14:45 +0000 Subject: [PATCH 53/61] Fix 'Expored variable innerParseJsonSchema has or is using name XXX from external module YYY but cannot be named' by exporting XXX from YYY --- ark/jsonschema/scope.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ark/jsonschema/scope.ts b/ark/jsonschema/scope.ts index 183c1d20e6..9b311e20b7 100644 --- a/ark/jsonschema/scope.ts +++ b/ark/jsonschema/scope.ts @@ -4,7 +4,7 @@ type AnyKeywords = { const?: unknown enum?: unknown[] } -type CompositionKeywords = { +export type CompositionKeywords = { allOf?: Schema[] anyOf?: Schema[] oneOf?: Schema[] @@ -24,7 +24,7 @@ type BaseSchema = | AnyKeywords | CompositionKeywords type Schema = BaseSchema | BaseSchema[] -type ArraySchema = { +export type ArraySchema = { additionalItems?: Schema contains?: Schema // JSON Schema states that if 'items' is not present, then treat as an empty schema (i.e. accept any valid JSON) @@ -45,7 +45,7 @@ type NumberSchema = { multipleOf?: number type: "number" | "integer" } -type ObjectSchema = { +export type ObjectSchema = { additionalProperties?: Schema maxProperties?: number minProperties?: number From 8dc58260bfa2fbe393be1d0b3738b8c229188495 Mon Sep 17 00:00:00 2001 From: TizzySaurus Date: Tue, 29 Oct 2024 21:16:11 +0000 Subject: [PATCH 54/61] Fix types for innerParseJsonSchema --- ark/jsonschema/json.ts | 115 ++++++++++++++++++++--------------------- 1 file changed, 57 insertions(+), 58 deletions(-) diff --git a/ark/jsonschema/json.ts b/ark/jsonschema/json.ts index 4c9d51e437..01cea28e0e 100644 --- a/ark/jsonschema/json.ts +++ b/ark/jsonschema/json.ts @@ -1,5 +1,5 @@ import { printable, throwParseError } from "@ark/util" -import { type, type Out, type Type } from "arktype" +import { type } from "arktype" import { parseJsonSchemaAnyKeywords } from "./any.ts" import { validateJsonSchemaArray } from "./array.ts" import { parseJsonSchemaCompositionKeywords } from "./composition.ts" @@ -8,70 +8,69 @@ import { validateJsonSchemaObject } from "./object.ts" import { JsonSchema } from "./scope.ts" import { validateJsonSchemaString } from "./string.ts" -export const innerParseJsonSchema: Type< - (In: JsonSchema.Schema) => Out>, - any -> = JsonSchema.Schema.pipe(jsonSchema => { - if (typeof jsonSchema === "boolean") { - if (jsonSchema) return JsonSchema.Json - else return type("never") // No runtime value ever passes validation for JSON schema of 'false' - } +export const innerParseJsonSchema = JsonSchema.Schema.pipe( + (jsonSchema: JsonSchema.Schema): type.Any => { + if (typeof jsonSchema === "boolean") { + if (jsonSchema) return JsonSchema.Json + else return type("never") // No runtime value ever passes validation for JSON schema of 'false' + } - if (Array.isArray(jsonSchema)) { - return ( - parseJsonSchemaCompositionKeywords({ anyOf: jsonSchema }) ?? - throwParseError( - "Failed to convert root array of JSON Schemas to an anyOf schema" + if (Array.isArray(jsonSchema)) { + return ( + parseJsonSchemaCompositionKeywords({ anyOf: jsonSchema }) ?? + throwParseError( + "Failed to convert root array of JSON Schemas to an anyOf schema" + ) ) - ) - } + } - const constAndOrEnumValidator = parseJsonSchemaAnyKeywords(jsonSchema) - const compositionValidator = parseJsonSchemaCompositionKeywords(jsonSchema) + const constAndOrEnumValidator = parseJsonSchemaAnyKeywords(jsonSchema) + const compositionValidator = parseJsonSchemaCompositionKeywords(jsonSchema) - const preTypeValidator: Type | undefined = - constAndOrEnumValidator ? - compositionValidator ? compositionValidator.and(constAndOrEnumValidator) - : constAndOrEnumValidator - : compositionValidator + const preTypeValidator: type.Any | undefined = + constAndOrEnumValidator ? + compositionValidator ? compositionValidator.and(constAndOrEnumValidator) + : constAndOrEnumValidator + : compositionValidator - if ("type" in jsonSchema) { - let typeValidator: Type + if ("type" in jsonSchema) { + let typeValidator: type.Any - switch (jsonSchema.type) { - case "array": - typeValidator = validateJsonSchemaArray.assert(jsonSchema) as never // A bug in ArkType makes this cast necessary - break - case "boolean": - case "null": - typeValidator = type(jsonSchema.type) - break - case "integer": - case "number": - typeValidator = validateJsonSchemaNumber.assert(jsonSchema) as never // A bug in ArkType makes this cast necessary - break - case "object": - typeValidator = validateJsonSchemaObject.assert(jsonSchema) as never // A bug in ArkType makes this cast necessary - break - case "string": - typeValidator = validateJsonSchemaString.assert(jsonSchema) as never // A bug in ArkType makes this cast necessary - break - default: - throwParseError( - // @ts-expect-error -- All valid 'type' values should be handled above - `Provided 'type' value must be a supported JSON Schema type (was '${jsonSchema.type}')` - ) + switch (jsonSchema.type) { + case "array": + typeValidator = validateJsonSchemaArray.assert(jsonSchema) + break + case "boolean": + case "null": + typeValidator = type(jsonSchema.type) + break + case "integer": + case "number": + typeValidator = validateJsonSchemaNumber.assert(jsonSchema) + break + case "object": + typeValidator = validateJsonSchemaObject.assert(jsonSchema) + break + case "string": + typeValidator = validateJsonSchemaString.assert(jsonSchema) + break + default: + throwParseError( + // @ts-expect-error -- All valid 'type' values should be handled above + `Provided 'type' value must be a supported JSON Schema type (was '${jsonSchema.type}')` + ) + } + if (preTypeValidator === undefined) return typeValidator + return typeValidator.and(preTypeValidator) } - if (preTypeValidator === undefined) return typeValidator - return typeValidator.and(preTypeValidator) - } - if (preTypeValidator === undefined) { - throwParseError( - `Provided JSON Schema must have one of 'type', 'enum', 'const', 'allOf', 'anyOf' but was ${printable(jsonSchema)}.` - ) + if (preTypeValidator === undefined) { + throwParseError( + `Provided JSON Schema must have one of 'type', 'enum', 'const', 'allOf', 'anyOf' but was ${printable(jsonSchema)}.` + ) + } + return preTypeValidator } - return preTypeValidator -}) +) -export const parseJsonSchema = (jsonSchema: JsonSchema.Schema): Type => +export const parseJsonSchema = (jsonSchema: JsonSchema.Schema): type.Any => innerParseJsonSchema.assert(jsonSchema) as never From 9826c00b83e351dbc9994f9db3a75fcac73bc5a9 Mon Sep 17 00:00:00 2001 From: TizzySaurus Date: Sun, 17 Nov 2024 21:47:49 +0000 Subject: [PATCH 55/61] add getDuplicatesOf array util to @ark/util, bumping the package version from 0.23.0 to 0.24.0 --- ark/util/arrays.ts | 28 ++++++++++++++++++++++++++++ ark/util/package.json | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/ark/util/arrays.ts b/ark/util/arrays.ts index e14c6bb8fb..c6785753b9 100644 --- a/ark/util/arrays.ts +++ b/ark/util/arrays.ts @@ -3,6 +3,34 @@ import type { anyOrNever, conform } from "./generics.ts" import type { isDisjoint } from "./intersections.ts" import type { parseNonNegativeInteger } from "./numbers.ts" +type DuplicateData = { element: val; indices: number[] } + +/** + * Extracts duplicated elements and their indices from an array, returning them. + * + * @param arr The array to extract duplicate elements from. + */ export const getDuplicatesOf = ( + arr: arr, + opts?: ComparisonOptions +): DuplicateData[] => { + const isEqual = opts?.isEqual ?? ((l, r) => l === r) + + const seenElements: Set = new Set() + const duplicates: DuplicateData[] = [] + + arr.forEach((element, indx) => { + if (seenElements.has(element)) { + const duplicatesEntryIndx = duplicates.findIndex((l, r) => + isEqual(l.element, r) + ) + if (duplicatesEntryIndx === -1) + duplicates.push({ element, indices: [indx] }) + else duplicates[duplicatesEntryIndx].indices.push(indx) + } else seenElements.add(element) + }) + return duplicates +} + export type pathToString< segments extends string[], delimiter extends string = "/" diff --git a/ark/util/package.json b/ark/util/package.json index ace5135acc..f9c92e827a 100644 --- a/ark/util/package.json +++ b/ark/util/package.json @@ -1,6 +1,6 @@ { "name": "@ark/util", - "version": "0.23.0", + "version": "0.24.0", "license": "MIT", "author": { "name": "David Blass", From 5272e44c4502d287894886aba07e84b136565bc3 Mon Sep 17 00:00:00 2001 From: TizzySaurus Date: Sun, 17 Nov 2024 21:48:36 +0000 Subject: [PATCH 56/61] Update JSON Schema object parsing logic to use new getDuplicatesOf util for improved error message upon duplicated 'required' keys --- ark/jsonschema/object.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ark/jsonschema/object.ts b/ark/jsonschema/object.ts index 1b17974201..7582afb39f 100644 --- a/ark/jsonschema/object.ts +++ b/ark/jsonschema/object.ts @@ -5,7 +5,7 @@ import { type Predicate, type TraversalContext } from "@ark/schema" -import { conflatenateAll, printable } from "@ark/util" +import { conflatenateAll, getDuplicatesOf, printable } from "@ark/util" import type { Out, Type } from "arktype" import { parseJsonSchema } from "./json.ts" @@ -138,11 +138,12 @@ const parseRequiredAndOptionalKeys = ( const requiredKeys: string[] = [] if ("properties" in jsonSchema) { if ("required" in jsonSchema) { - if (jsonSchema.required.length !== new Set(jsonSchema.required).size) { + const duplicateRequiredKeys = getDuplicatesOf(jsonSchema.required) + if (duplicateRequiredKeys.length !== 0) { ctx.reject({ path: ["required"], expected: "an array of unique strings", - actual: printable(jsonSchema.required) + actual: `an array with the following duplicates: ${printable(duplicateRequiredKeys)}` }) } From 6221890c6e0ed5a5cfc9798627259fb87b68814e Mon Sep 17 00:00:00 2001 From: Izan Robinson Date: Tue, 3 Dec 2024 20:37:44 +0000 Subject: [PATCH 57/61] Bump @ark/util version from 0.25.0 to 0.26.0 due to new getDuplicatesOf array util --- ark/util/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ark/util/package.json b/ark/util/package.json index 550e0ed022..fec368e0a3 100644 --- a/ark/util/package.json +++ b/ark/util/package.json @@ -1,6 +1,6 @@ { "name": "@ark/util", - "version": "0.25.0", + "version": "0.26.0", "license": "MIT", "author": { "name": "David Blass", From 20f446b30febfac442e0d0a91b7da0200be23c32 Mon Sep 17 00:00:00 2001 From: TizzySaurus Date: Tue, 3 Dec 2024 21:47:21 +0000 Subject: [PATCH 58/61] Add support for JSON Schema 'prefixItems' keyword on arrays --- ark/jsonschema/array.ts | 12 +++++++++++- ark/jsonschema/scope.ts | 6 ++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/ark/jsonschema/array.ts b/ark/jsonschema/array.ts index 1a471f3cec..c25cef0ec4 100644 --- a/ark/jsonschema/array.ts +++ b/ark/jsonschema/array.ts @@ -55,11 +55,21 @@ const arrayContainsItemMatchingSchema = ( export const validateJsonSchemaArray: Type< (In: JsonSchema.ArraySchema) => Out>, any -> = JsonSchema.ArraySchema.pipe(jsonSchema => { +> = JsonSchema.ArraySchema.pipe((jsonSchema, ctx) => { const arktypeArraySchema: Intersection.Schema> = { proto: "Array" } + if ("prefixItems" in jsonSchema) { + if ("items" in jsonSchema) { + ctx.reject({ + expected: "a valid array JSON Schema", + actual: + "an array JSON Schema with mutually exclusive keys 'prefixItems' and 'items' specified" + }) + } else jsonSchema.items = jsonSchema.prefixItems + } + if ("items" in jsonSchema) { if (Array.isArray(jsonSchema.items)) { arktypeArraySchema.sequence = { diff --git a/ark/jsonschema/scope.ts b/ark/jsonschema/scope.ts index 9b311e20b7..1e15e62436 100644 --- a/ark/jsonschema/scope.ts +++ b/ark/jsonschema/scope.ts @@ -31,6 +31,9 @@ export type ArraySchema = { items?: Schema | Schema[] maxItems?: number minItems?: number + // NB: Technically `prefixItems` and `items` are mutually exclusive, + // which is reflected at runtime but it's not worth the performance cost to validate this statically. + prefixItems?: Schema[] type: "array" uniqueItems?: boolean } @@ -105,6 +108,9 @@ const $: JsonSchemaScope = scope({ "items?": "Schema|Schema[]", "maxItems?": "number.integer>=0", "minItems?": "number.integer>=0", + // NB: Technically `prefixItems` and `items` are mutually exclusive, + // which is reflected at runtime but it's not worth the performance cost to validate this statically. + "prefixItems?": "Schema[]", type: "'array'", "uniqueItems?": "boolean" }, From 0fac361b60253258046da9445ab90bf8b43b3792 Mon Sep 17 00:00:00 2001 From: TizzySaurus Date: Tue, 3 Dec 2024 21:56:02 +0000 Subject: [PATCH 59/61] Add unit tests for new 'prefixItems' support --- ark/jsonschema/__tests__/array.test.ts | 30 ++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/ark/jsonschema/__tests__/array.test.ts b/ark/jsonschema/__tests__/array.test.ts index 29f0670cba..07c16ff134 100644 --- a/ark/jsonschema/__tests__/array.test.ts +++ b/ark/jsonschema/__tests__/array.test.ts @@ -10,20 +10,38 @@ contextualize(() => { attest(t.json).snap({ proto: "Array" }) }) - it("items & additionalItems", () => { - const tItems = parseJsonSchema({ + it("items & prefixItems", () => { + const tItems = parseJsonSchema({ type: "array", items: { type: "string" } }) + attest(tItems.json).snap({ proto: "Array", sequence: "string" }) + attest(tItems.allows(["foo"])).equals(true) + attest(tItems.allows(["foo", "bar"])).equals(true) + attest(tItems.allows(["foo", 3, "bar"])).equals(false) + + const tItemsArr = parseJsonSchema({ type: "array", items: [{ type: "string" }, { type: "number" }] }) - attest(tItems.json).snap({ + attest(tItemsArr.json).snap({ proto: "Array", sequence: { prefix: ["string", "number"] }, exactLength: 2 }) - attest(tItems.allows(["foo", 1])).equals(true) - attest(tItems.allows([1, "foo"])).equals(false) - attest(tItems.allows(["foo", 1, true])).equals(false) + attest(tItemsArr.allows(["foo", 1])).equals(true) + attest(tItemsArr.allows([1, "foo"])).equals(false) + attest(tItemsArr.allows(["foo", 1, true])).equals(false) + + const tPrefixItems = parseJsonSchema({ + type: "array", + prefixItems: [{ type: "string" }, { type: "number" }] + }) + attest(tPrefixItems.json).snap({ + proto: "Array", + sequence: { prefix: ["string", "number"] }, + exactLength: 2 + }) + }) + it("additionalItems", () => { const tItemsVariadic = parseJsonSchema({ type: "array", items: [{ type: "string" }, { type: "number" }], From 65112a1688c5b651d50cb62c6e2acc0a501d4922 Mon Sep 17 00:00:00 2001 From: TizzySaurus Date: Tue, 3 Dec 2024 22:29:56 +0000 Subject: [PATCH 60/61] Make JSON Schema types shared between arktype and @ark/jsonschema --- ark/jsonschema/README.md | 34 ++++++++--- ark/jsonschema/any.ts | 9 +-- ark/jsonschema/array.ts | 6 +- ark/jsonschema/composition.ts | 17 ++---- ark/jsonschema/json.ts | 73 ++++++++++++++--------- ark/jsonschema/number.ts | 8 +-- ark/jsonschema/object.ts | 18 +++--- ark/jsonschema/scope.ts | 100 +++++++------------------------- ark/jsonschema/string.ts | 6 +- ark/schema/shared/jsonSchema.ts | 56 +++++++++++++++--- 10 files changed, 168 insertions(+), 159 deletions(-) diff --git a/ark/jsonschema/README.md b/ark/jsonschema/README.md index 24eef57370..a89bc13962 100644 --- a/ark/jsonschema/README.md +++ b/ark/jsonschema/README.md @@ -1,33 +1,51 @@ # @arktype/jsonschema ## What is it? + @arktype/jsonschema is a package that allows converting from a JSON Schema schema, to an ArkType type. For example: + ```js import { parseJsonSchema } from "@ark/jsonschema" -const t = parseJsonSchema({type: "string", minLength: 5, maxLength: 10}) +const t = parseJsonSchema({ type: "string", minLength: 5, maxLength: 10 }) ``` + is equivalent to: + ```js import { type } from "arktype" const t = type("5<=string<=10") ``` + This enables easy adoption of ArkType for people who currently have JSON Schema based runtime validation in their codebase. -Where possible, the library also has TypeScript type inference so that the runtime validation remains typesafe. Extending on the above example, this means that the return type of the below `parseString` function would be correctly inferred as `string`: +Where possible, the library also has TypeScript type inference so that the runtime validation remains typesafe. Extending on the above example, this means that the return type of the below `parseString` function would be correctly inferred as `string`: + ```ts const assertIsString = (data: unknown) return t.assert(data) ``` ## Extra Type Safety -If you wish to ensure that your JSON Schema schemas are valid, you can do this too! Simply import the `JsonSchema` namespace type from `@ark/jsonschema`, and use the appropriate member like so: + +If you wish to ensure that your JSON Schema schemas are valid, you can do this too! Simply import the relevant `Schema` type from `@ark/jsonschema` like so: + ```ts -import type { JsonSchema } from "@ark/jsonschema" +import type { JsonSchema } from "arktype" -const schema: JsonSchema.StringSchema = { - type: "string", - minLength: "3" // errors stating that 'minLength' must be a number +const integerSchema: JsonSchema.Numeric = { + type: "integer", + multipleOf: "3" // errors stating that 'multipleOf' must be a number } -``` \ No newline at end of file +``` + +Note that for string schemas exclusively, you must import the schema type from `@ark/jsonschema` instead of `arktype`. This is because `@ark/jsonschema` doesn't yet support the `format` keyword whilst `arktype` does. + +```ts +import type { StringSchema } from "@ark/jsonschema" +const stringSchema: StringSchema = { + type: "string", + minLength: "3" // errors stating that 'minLength' must be a number +} +``` diff --git a/ark/jsonschema/any.ts b/ark/jsonschema/any.ts index c2c1d99505..5e45be6771 100644 --- a/ark/jsonschema/any.ts +++ b/ark/jsonschema/any.ts @@ -1,13 +1,8 @@ import { throwParseError } from "@ark/util" -import { type Type, type } from "arktype" -import type { JsonSchema } from "./scope.ts" +import { type JsonSchema, type Type, type } from "arktype" export const parseJsonSchemaAnyKeywords = ( - jsonSchema: - | JsonSchema.TypeWithNoKeywords - | JsonSchema.TypeWithKeywords - | JsonSchema.AnyKeywords - | JsonSchema.CompositionKeywords + jsonSchema: JsonSchema ): Type | undefined => { if ("const" in jsonSchema) { if ("enum" in jsonSchema) { diff --git a/ark/jsonschema/array.ts b/ark/jsonschema/array.ts index c25cef0ec4..775fe8b642 100644 --- a/ark/jsonschema/array.ts +++ b/ark/jsonschema/array.ts @@ -8,7 +8,7 @@ import { printable } from "@ark/util" import type { Out, Type } from "arktype" import { parseJsonSchema } from "./json.ts" -import { JsonSchema } from "./scope.ts" +import { JsonSchemaScope, type ArraySchema } from "./scope.ts" const deepNormalize = (data: unknown): unknown => typeof data === "object" ? @@ -53,9 +53,9 @@ const arrayContainsItemMatchingSchema = ( }) export const validateJsonSchemaArray: Type< - (In: JsonSchema.ArraySchema) => Out>, + (In: ArraySchema) => Out>, any -> = JsonSchema.ArraySchema.pipe((jsonSchema, ctx) => { +> = JsonSchemaScope.ArraySchema.pipe((jsonSchema, ctx) => { const arktypeArraySchema: Intersection.Schema> = { proto: "Array" } diff --git a/ark/jsonschema/composition.ts b/ark/jsonschema/composition.ts index be5daf74af..7ca32134d8 100644 --- a/ark/jsonschema/composition.ts +++ b/ark/jsonschema/composition.ts @@ -1,19 +1,18 @@ import { printable } from "@ark/util" -import { type, type Type } from "arktype" +import { type, type JsonSchema, type Type } from "arktype" import { parseJsonSchema } from "./json.ts" -import type { JsonSchema } from "./scope.ts" -const validateAllOfJsonSchemas = (jsonSchemas: JsonSchema.Schema[]): Type => +const validateAllOfJsonSchemas = (jsonSchemas: readonly JsonSchema[]): Type => jsonSchemas .map(jsonSchema => parseJsonSchema(jsonSchema)) .reduce((acc, validator) => acc.and(validator)) -const validateAnyOfJsonSchemas = (jsonSchemas: JsonSchema.Schema[]): Type => +const validateAnyOfJsonSchemas = (jsonSchemas: readonly JsonSchema[]): Type => jsonSchemas .map(jsonSchema => parseJsonSchema(jsonSchema)) .reduce((acc, validator) => acc.or(validator)) -const validateNotJsonSchema = (jsonSchema: JsonSchema.Schema) => { +const validateNotJsonSchema = (jsonSchema: JsonSchema) => { const inner = parseJsonSchema(jsonSchema) return type("unknown").narrow((data, ctx) => inner.allows(data) ? @@ -25,7 +24,7 @@ const validateNotJsonSchema = (jsonSchema: JsonSchema.Schema) => { ) as Type } -const validateOneOfJsonSchemas = (jsonSchemas: JsonSchema.Schema[]) => { +const validateOneOfJsonSchemas = (jsonSchemas: readonly JsonSchema[]) => { const oneOfValidators = jsonSchemas.map(nestedSchema => parseJsonSchema(nestedSchema) ) @@ -58,11 +57,7 @@ const validateOneOfJsonSchemas = (jsonSchemas: JsonSchema.Schema[]) => { } export const parseJsonSchemaCompositionKeywords = ( - jsonSchema: - | JsonSchema.TypeWithNoKeywords - | JsonSchema.TypeWithKeywords - | JsonSchema.AnyKeywords - | JsonSchema.CompositionKeywords + jsonSchema: JsonSchema ): Type | undefined => { if ("allOf" in jsonSchema) return validateAllOfJsonSchemas(jsonSchema.allOf) if ("anyOf" in jsonSchema) return validateAnyOfJsonSchemas(jsonSchema.anyOf) diff --git a/ark/jsonschema/json.ts b/ark/jsonschema/json.ts index 01cea28e0e..7b15a88f1c 100644 --- a/ark/jsonschema/json.ts +++ b/ark/jsonschema/json.ts @@ -1,17 +1,18 @@ +import type { JsonSchemaOrBoolean } from "@ark/schema" import { printable, throwParseError } from "@ark/util" -import { type } from "arktype" +import { type, type JsonSchema } from "arktype" import { parseJsonSchemaAnyKeywords } from "./any.ts" import { validateJsonSchemaArray } from "./array.ts" import { parseJsonSchemaCompositionKeywords } from "./composition.ts" import { validateJsonSchemaNumber } from "./number.ts" import { validateJsonSchemaObject } from "./object.ts" -import { JsonSchema } from "./scope.ts" +import { JsonSchemaScope } from "./scope.ts" import { validateJsonSchemaString } from "./string.ts" -export const innerParseJsonSchema = JsonSchema.Schema.pipe( - (jsonSchema: JsonSchema.Schema): type.Any => { +export const innerParseJsonSchema = JsonSchemaScope.Schema.pipe( + (jsonSchema: JsonSchemaOrBoolean): type.Any => { if (typeof jsonSchema === "boolean") { - if (jsonSchema) return JsonSchema.Json + if (jsonSchema) return JsonSchemaScope.Json else return type("never") // No runtime value ever passes validation for JSON schema of 'false' } @@ -24,8 +25,12 @@ export const innerParseJsonSchema = JsonSchema.Schema.pipe( ) } - const constAndOrEnumValidator = parseJsonSchemaAnyKeywords(jsonSchema) - const compositionValidator = parseJsonSchemaCompositionKeywords(jsonSchema) + const constAndOrEnumValidator = parseJsonSchemaAnyKeywords( + jsonSchema as JsonSchema + ) + const compositionValidator = parseJsonSchemaCompositionKeywords( + jsonSchema as JsonSchema + ) const preTypeValidator: type.Any | undefined = constAndOrEnumValidator ? @@ -36,29 +41,39 @@ export const innerParseJsonSchema = JsonSchema.Schema.pipe( if ("type" in jsonSchema) { let typeValidator: type.Any - switch (jsonSchema.type) { - case "array": - typeValidator = validateJsonSchemaArray.assert(jsonSchema) - break - case "boolean": - case "null": - typeValidator = type(jsonSchema.type) - break - case "integer": - case "number": - typeValidator = validateJsonSchemaNumber.assert(jsonSchema) - break - case "object": - typeValidator = validateJsonSchemaObject.assert(jsonSchema) - break - case "string": - typeValidator = validateJsonSchemaString.assert(jsonSchema) - break - default: + if (Array.isArray(jsonSchema.type)) { + typeValidator = + parseJsonSchemaCompositionKeywords({ + anyOf: jsonSchema.type.map(t => ({ type: t })) + }) ?? throwParseError( - // @ts-expect-error -- All valid 'type' values should be handled above - `Provided 'type' value must be a supported JSON Schema type (was '${jsonSchema.type}')` + "Failed to convert array of JSON Schemas types to an anyOf schema" ) + } else { + const jsonSchemaType = jsonSchema.type as JsonSchema.TypeName + switch (jsonSchemaType) { + case "array": + typeValidator = validateJsonSchemaArray.assert(jsonSchema) + break + case "boolean": + case "null": + typeValidator = type(jsonSchemaType) + break + case "integer": + case "number": + typeValidator = validateJsonSchemaNumber.assert(jsonSchema) + break + case "object": + typeValidator = validateJsonSchemaObject.assert(jsonSchema) + break + case "string": + typeValidator = validateJsonSchemaString.assert(jsonSchema) + break + default: + throwParseError( + `Provided 'type' value must be a supported JSON Schema type (was '${jsonSchemaType}')` + ) + } } if (preTypeValidator === undefined) return typeValidator return typeValidator.and(preTypeValidator) @@ -72,5 +87,5 @@ export const innerParseJsonSchema = JsonSchema.Schema.pipe( } ) -export const parseJsonSchema = (jsonSchema: JsonSchema.Schema): type.Any => +export const parseJsonSchema = (jsonSchema: JsonSchemaOrBoolean): type.Any => innerParseJsonSchema.assert(jsonSchema) as never diff --git a/ark/jsonschema/number.ts b/ark/jsonschema/number.ts index 733ed4989a..623dd11cb4 100644 --- a/ark/jsonschema/number.ts +++ b/ark/jsonschema/number.ts @@ -1,12 +1,12 @@ import { rootSchema, type Intersection } from "@ark/schema" import { throwParseError } from "@ark/util" -import type { Out, Type } from "arktype" -import { JsonSchema } from "./scope.ts" +import type { JsonSchema, Out, Type } from "arktype" +import { JsonSchemaScope } from "./scope.ts" export const validateJsonSchemaNumber: Type< - (In: JsonSchema.NumberSchema) => Out>, + (In: JsonSchema.Numeric) => Out>, any -> = JsonSchema.NumberSchema.pipe((jsonSchema): Type => { +> = JsonSchemaScope.NumberSchema.pipe((jsonSchema): Type => { const arktypeNumberSchema: Intersection.Schema = { domain: "number" } diff --git a/ark/jsonschema/object.ts b/ark/jsonschema/object.ts index 7582afb39f..9ea50bbabe 100644 --- a/ark/jsonschema/object.ts +++ b/ark/jsonschema/object.ts @@ -6,13 +6,13 @@ import { type TraversalContext } from "@ark/schema" import { conflatenateAll, getDuplicatesOf, printable } from "@ark/util" -import type { Out, Type } from "arktype" +import type { JsonSchema, Out, Type } from "arktype" import { parseJsonSchema } from "./json.ts" -import { JsonSchema } from "./scope.ts" +import { JsonSchemaScope } from "./scope.ts" const parseMinMaxProperties = ( - jsonSchema: JsonSchema.ObjectSchema, + jsonSchema: JsonSchema.Object, ctx: TraversalContext ) => { const predicates: Predicate.Schema[] = [] @@ -51,7 +51,7 @@ const parseMinMaxProperties = ( } const parsePatternProperties = ( - jsonSchema: JsonSchema.ObjectSchema, + jsonSchema: JsonSchema.Object, ctx: TraversalContext ) => { if (!("patternProperties" in jsonSchema)) return @@ -98,7 +98,7 @@ const parsePatternProperties = ( } const parsePropertyNames = ( - jsonSchema: JsonSchema.ObjectSchema, + jsonSchema: JsonSchema.Object, ctx: TraversalContext ) => { if (!("propertyNames" in jsonSchema)) return @@ -131,7 +131,7 @@ const parsePropertyNames = ( } const parseRequiredAndOptionalKeys = ( - jsonSchema: JsonSchema.ObjectSchema, + jsonSchema: JsonSchema.Object, ctx: TraversalContext ) => { const optionalKeys: string[] = [] @@ -183,7 +183,7 @@ const parseRequiredAndOptionalKeys = ( } } -const parseAdditionalProperties = (jsonSchema: JsonSchema.ObjectSchema) => { +const parseAdditionalProperties = (jsonSchema: JsonSchema.Object) => { if (!("additionalProperties" in jsonSchema)) return const properties = @@ -229,9 +229,9 @@ const parseAdditionalProperties = (jsonSchema: JsonSchema.ObjectSchema) => { } export const validateJsonSchemaObject: Type< - (In: JsonSchema.ObjectSchema) => Out>, + (In: JsonSchema.Object) => Out>, any -> = JsonSchema.ObjectSchema.pipe((jsonSchema, ctx): Type => { +> = JsonSchemaScope.ObjectSchema.pipe((jsonSchema, ctx): Type => { const arktypeObjectSchema: Intersection.Schema = { domain: "object" } diff --git a/ark/jsonschema/scope.ts b/ark/jsonschema/scope.ts index 1e15e62436..2567ba30a9 100644 --- a/ark/jsonschema/scope.ts +++ b/ark/jsonschema/scope.ts @@ -1,79 +1,37 @@ -import { scope, type Scope } from "arktype" +import type { JsonSchemaOrBoolean } from "@ark/schema" +import { type JsonSchema, scope, type Scope } from "arktype" + +type AnyKeywords = Partial -type AnyKeywords = { - const?: unknown - enum?: unknown[] -} -export type CompositionKeywords = { - allOf?: Schema[] - anyOf?: Schema[] - oneOf?: Schema[] - not?: Schema -} type TypeWithNoKeywords = { type: "boolean" | "null" } -type TypeWithKeywords = ArraySchema | NumberSchema | ObjectSchema | StringSchema +type TypeWithKeywords = + | JsonSchema.Array + | JsonSchema.Numeric + | JsonSchema.Object + | StringSchema // NB: For sake of simplicitly, at runtime it's assumed that // whatever we're parsing is valid JSON since it will be 99% of the time. // This decision may be changed later, e.g. when a built-in JSON type exists in AT. type Json = unknown -type BaseSchema = - // NB: `true` means "accept an valid JSON"; `false` means "reject everything". - | boolean - | TypeWithNoKeywords - | TypeWithKeywords - | AnyKeywords - | CompositionKeywords -type Schema = BaseSchema | BaseSchema[] -export type ArraySchema = { - additionalItems?: Schema - contains?: Schema - // JSON Schema states that if 'items' is not present, then treat as an empty schema (i.e. accept any valid JSON) - items?: Schema | Schema[] - maxItems?: number - minItems?: number - // NB: Technically `prefixItems` and `items` are mutually exclusive, - // which is reflected at runtime but it's not worth the performance cost to validate this statically. - prefixItems?: Schema[] - type: "array" - uniqueItems?: boolean -} -type NumberSchema = { - // NB: Technically 'exclusiveMaximum' and 'exclusiveMinimum' are mutually exclusive with 'maximum' and 'minimum', respectively, - // which is reflected at runtime but it's not worth the performance cost to validate this statically. - exclusiveMaximum?: number - exclusiveMinimum?: number - maximum?: number - minimum?: number - // NB: JSON Schema allows decimal multipleOf, but ArkType only supports integer. - multipleOf?: number - type: "number" | "integer" -} -export type ObjectSchema = { - additionalProperties?: Schema - maxProperties?: number - minProperties?: number - patternProperties?: { [k: string]: Schema } - // NB: Technically 'properties' is required when 'required' is present, - // which is reflected at runtime but it's not worth the performance cost to validate this statically. - properties?: { [k: string]: Schema } - propertyNames?: Schema - required?: string[] - type: "object" -} -type StringSchema = { - maxLength?: number - minLength?: number - pattern?: RegExp | string - type: "string" + +type ArraySchema = JsonSchema.Array + +type NumberSchema = JsonSchema.Numeric + +type ObjectSchema = JsonSchema.Object + +// NB: @ark/jsonschema doesn't support the "format" keyword, and the "pattern" could be string|RegExp rather than only string, so we need a separate type +export type StringSchema = Omit & { + pattern?: string | RegExp } type JsonSchemaScope = Scope<{ AnyKeywords: AnyKeywords - CompositionKeywords: CompositionKeywords + CompositionKeywords: JsonSchema.Composition TypeWithNoKeywords: TypeWithNoKeywords TypeWithKeywords: TypeWithKeywords Json: Json - Schema: Schema + Schema: JsonSchemaOrBoolean ArraySchema: ArraySchema NumberSchema: NumberSchema ObjectSchema: ObjectSchema @@ -144,18 +102,4 @@ const $: JsonSchemaScope = scope({ type: "'string'" } }) as unknown as JsonSchemaScope -export const JsonSchema = $.export() - -export declare namespace JsonSchema { - export type $ = typeof $ - export type Schema = typeof JsonSchema.Schema.infer - export type Json = typeof JsonSchema.Json.infer - export type AnyKeywords = typeof JsonSchema.AnyKeywords.infer - export type CompositionKeywords = typeof JsonSchema.CompositionKeywords.infer - export type TypeWithKeywords = typeof JsonSchema.TypeWithKeywords.infer - export type TypeWithNoKeywords = typeof JsonSchema.TypeWithNoKeywords.infer - export type ArraySchema = typeof JsonSchema.ArraySchema.infer - export type NumberSchema = typeof JsonSchema.NumberSchema.infer - export type ObjectSchema = typeof JsonSchema.ObjectSchema.infer - export type StringSchema = typeof JsonSchema.StringSchema.infer -} +export const JsonSchemaScope = $.export() diff --git a/ark/jsonschema/string.ts b/ark/jsonschema/string.ts index 88a928a75b..8aa835f41c 100644 --- a/ark/jsonschema/string.ts +++ b/ark/jsonschema/string.ts @@ -1,11 +1,11 @@ import { rootSchema, type Intersection } from "@ark/schema" import type { Out, Type } from "arktype" -import { JsonSchema } from "./scope.ts" +import { JsonSchemaScope, type StringSchema } from "./scope.ts" export const validateJsonSchemaString: Type< - (In: JsonSchema.StringSchema) => Out>, + (In: StringSchema) => Out>, any -> = JsonSchema.StringSchema.pipe((jsonSchema): Type => { +> = JsonSchemaScope.StringSchema.pipe((jsonSchema): Type => { const arktypeStringSchema: Intersection.Schema = { domain: "string" } diff --git a/ark/schema/shared/jsonSchema.ts b/ark/schema/shared/jsonSchema.ts index 4709223423..b3eee3ed3d 100644 --- a/ark/schema/shared/jsonSchema.ts +++ b/ark/schema/shared/jsonSchema.ts @@ -1,13 +1,16 @@ import { printable, throwInternalError, + type array, type JsonArray, type JsonObject, type listable } from "@ark/util" import type { ConstraintKind } from "./implement.ts" -export type JsonSchema = JsonSchema.Union | JsonSchema.Branch +export type JsonSchema = JsonSchema.NonBooleanBranch +export type ListableJsonSchema = listable +export type JsonSchemaOrBoolean = listable export declare namespace JsonSchema { export type TypeName = @@ -31,30 +34,61 @@ export declare namespace JsonSchema { examples?: readonly t[] } - export type Branch = Constrainable | Const | String | Numeric | Object | Array + type Composition = Union | OneOf | Intersection | Not + + type NonBooleanBranch = + | Constrainable + | Const + | Composition + | Enum + | String + | Numeric + | Object + | Array + + export type Branch = boolean | JsonSchema export interface Constrainable extends Meta { type?: listable } + export interface Intersection extends Meta { + allOf: readonly JsonSchema[] + } + + export interface Not { + not: JsonSchema + } + + export interface OneOf extends Meta { + oneOf: readonly JsonSchema[] + } + export interface Union extends Meta { - anyOf: readonly Branch[] + anyOf: readonly JsonSchema[] } export interface Const extends Meta { const: unknown } + export interface Enum extends Meta { + enum: array + } + export interface String extends Meta { type: "string" minLength?: number maxLength?: number - pattern?: string + pattern?: string | RegExp format?: string } + // NB: Technically 'exclusiveMaximum' and 'exclusiveMinimum' are mutually exclusive with 'maximum' and 'minimum', respectively, + // which is reflected at runtime but it's not worth the performance cost to validate this statically. export interface Numeric extends Meta { type: "number" | "integer" + // NB: JSON Schema allows decimal multipleOf, but ArkType only supports integer. multipleOf?: number minimum?: number exclusiveMinimum?: number @@ -62,20 +96,28 @@ export declare namespace JsonSchema { exclusiveMaximum?: number } + // NB: Technically 'properties' is required when 'required' is present, + // which is reflected at runtime but it's not worth the performance cost to validate this statically. export interface Object extends Meta { type: "object" properties?: Record required?: string[] patternProperties?: Record - additionalProperties?: false | JsonSchema + additionalProperties?: JsonSchemaOrBoolean + maxProperties?: number + minProperties?: number + propertyNames?: String } export interface Array extends Meta { type: "array" + additionalItems?: JsonSchemaOrBoolean + contains?: JsonSchemaOrBoolean + uniqueItems?: boolean minItems?: number maxItems?: number - items?: JsonSchema | false - prefixItems?: readonly JsonSchema[] + items?: JsonSchemaOrBoolean + prefixItems?: readonly Branch[] } export type LengthBoundable = String | Array From 156786236c61987606b54d5a3d74685305678361 Mon Sep 17 00:00:00 2001 From: TizzySaurus Date: Tue, 3 Dec 2024 22:34:44 +0000 Subject: [PATCH 61/61] patch-fix broken 'arktype' and '@ark/util' unit tests --- ark/type/__tests__/imports.test.ts | 2 +- ark/util/registry.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ark/type/__tests__/imports.test.ts b/ark/type/__tests__/imports.test.ts index fb049c06c7..87aacdc783 100644 --- a/ark/type/__tests__/imports.test.ts +++ b/ark/type/__tests__/imports.test.ts @@ -76,8 +76,8 @@ contextualize(() => { // have to snapshot the module since TypeScript treats it as bivariant attest(types).type.toString.snap(`Module<{ - public: true | 3 | uuid | "no" hasCrept: true + public: true | 3 | uuid | "no" }>`) }) } diff --git a/ark/util/registry.ts b/ark/util/registry.ts index edeb954b62..9e203df908 100644 --- a/ark/util/registry.ts +++ b/ark/util/registry.ts @@ -7,7 +7,7 @@ import { FileConstructor, objectKindOf } from "./objectKinds.ts" // recent node versions (https://nodejs.org/api/esm.html#json-modules). // For now, we assert this matches the package.json version via a unit test. -export const arkUtilVersion = "0.25.0" +export const arkUtilVersion = "0.26.0" export const initialRegistryContents = { version: arkUtilVersion,