diff --git a/.changeset/config.json b/.changeset/config.json index 3be5056d1f..851482664e 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -10,6 +10,6 @@ "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", - "ignore": ["arktype", "@arktype/repo", "@arktype/docs", "@arktype/arkdark"], + "ignore": ["arktype", "@arktype/repo", "@arktype/docs", "arkdark"], "bumpVersionsWithWorkspaceProtocolOnly": true } diff --git a/.changeset/poor-buckets-pump.md b/.changeset/poor-buckets-pump.md new file mode 100644 index 0000000000..abeeb173e7 --- /dev/null +++ b/.changeset/poor-buckets-pump.md @@ -0,0 +1,5 @@ +--- +"@arktype/schema": patch +--- + +Pipe and narrow bug fixes (see [arktype CHANGELOG](../type/CHANGELOG.md)) diff --git a/ark/attest/package.json b/ark/attest/package.json index bc22c95d41..59fa9454f9 100644 --- a/ark/attest/package.json +++ b/ark/attest/package.json @@ -25,7 +25,7 @@ "bunTest": "bun test --preload ../repo/bunTestSetup.ts" }, "dependencies": { - "arktype": "2.0.0-dev.13", + "arktype": "latest", "@arktype/fs": "workspace:*", "@arktype/util": "workspace:*", "@typescript/vfs": "1.5.0", diff --git a/ark/dark/package.json b/ark/dark/package.json index 2781cdd488..7ad1ff2c46 100644 --- a/ark/dark/package.json +++ b/ark/dark/package.json @@ -1,9 +1,9 @@ { - "name": "@arktype/arkdark", + "name": "arkdark", "private": true, "displayName": "ArkDark", "description": "ArkType syntax highlighting and themeā›µ", - "version": "5.1.3", + "version": "5.1.4", "publisher": "arktypeio", "type": "module", "scripts": { @@ -52,15 +52,15 @@ }, "errorLens.replace": [ { - "matcher": "^(?:Type|Argument of type) '.*' is not assignable to type 'keyError<\"(.*)\">'\\.$", + "matcher": "^(?:Type|Argument of type) '.*' is not assignable to (?:parameter of )?type 'keyError<\"(.*)\">'\\.$", "message": "$1" }, { - "matcher": "^(?:Type|Argument of type) '.*' is not assignable to type '\"(.*\\u200A)\"'\\.$", + "matcher": "^(?:Type|Argument of type) '.*' is not assignable to (?:parameter of )?type '\"(.*\\u200A)\"'\\.$", "message": "$1" }, { - "matcher": "^(?:Type|Argument of type) '\"(.*)\"' is not assignable to type '(\"\\1.*\")'\\.$", + "matcher": "^(?:Type|Argument of type) '\"(.*)\"' is not assignable to (?:parameter of )?type '(\"\\1.*\")'\\.$", "message": "$2" }, { diff --git a/ark/docs/src/content/docs/index.mdx b/ark/docs/src/content/docs/index.mdx index 91d82bdefc..b7d1e22fba 100644 --- a/ark/docs/src/content/docs/index.mdx +++ b/ark/docs/src/content/docs/index.mdx @@ -37,11 +37,23 @@ import { HomeDemo } from "../../components/HomeDemo.tsx" Zod](https://moltar.github.io/typescript-runtime-type-benchmarks/) with editor performance that will remind you how autocomplete is supposed to feel - + Schemas are half as long and twice as readable with hovers that tell you just what really matters Deeply customizable and composable messages with great defaults + + ArkType uses set theory to understand and expose the relationships between + your types at runtime the way TypeScript does at compile time + + + Every schema you define is internally optimized to provide the clearest, + fastest validation possible + + {/* + Most definitions are just objects and strings- take them across the stack or + even outside JS altogether + */} diff --git a/ark/repo/scratch.ts b/ark/repo/scratch.ts index c050722a2d..a892d24f39 100644 --- a/ark/repo/scratch.ts +++ b/ark/repo/scratch.ts @@ -1,34 +1,18 @@ import { type } from "arktype" -const parseBigint = type("string", "=>", (s, ctx) => { - try { - return BigInt(s) - } catch { - return ctx.error("a valid number") - } +const user = type({ + name: "string", + age: "number" }) -// or +const parseUser = type("string").pipe(s => JSON.parse(s), user) -const parseBigint2 = type("string").pipe((s, ctx) => { - try { - return BigInt(s) - } catch { - return ctx.error("a valid number") - } -}) - -const Test = type({ - group: { - nested: { - value: parseBigint - } - } -}) +const validUser = parseUser(`{ "name": "David", "age": 30 }`) //? +// ^? -const myFunc = () => { - const out = Test({}) - if (out instanceof type.errors) return +const invalidUser = parseUser(`{ "name": "David" }`) +// ^? - const value: bigint = out.group.nested.value +if (invalidUser instanceof type.errors) { + console.log(invalidUser.summary) } diff --git a/ark/schema/node.ts b/ark/schema/node.ts index 86bff9cace..6d1fa05f1d 100644 --- a/ark/schema/node.ts +++ b/ark/schema/node.ts @@ -62,7 +62,10 @@ export abstract class BaseNode< ) return data - if (pipedFromCtx) return this.traverseApply(data, pipedFromCtx) + if (pipedFromCtx) { + this.traverseApply(data, pipedFromCtx) + return pipedFromCtx.data + } const ctx = new TraversalContext(data, this.$.resolvedConfig) this.traverseApply(data, ctx) diff --git a/ark/schema/shared/traversal.ts b/ark/schema/shared/traversal.ts index 42f42deb67..98455fb626 100644 --- a/ark/schema/shared/traversal.ts +++ b/ark/schema/shared/traversal.ts @@ -49,41 +49,42 @@ export class TraversalContext { finalize(): unknown { if (this.hasError()) return this.errors - if (this.queuedMorphs.length) { - for (let i = 0; i < this.queuedMorphs.length; i++) { - const { path, morphs } = this.queuedMorphs[i] + // invoking morphs that are Nodes will reuse this context, potentially + // adding additional morphs, so we have to continue looping until + // queuedMorphs is empty rather than iterating over the list once + while (this.queuedMorphs.length) { + const { path, morphs } = this.queuedMorphs.shift()! - const key = path.at(-1) + const key = path.at(-1) - let parent: any + let parent: any - if (key !== undefined) { - // find the object on which the key to be morphed exists - parent = this.root - for (let pathIndex = 0; pathIndex < path.length - 1; pathIndex++) - parent = parent[path[pathIndex]] - } + if (key !== undefined) { + // find the object on which the key to be morphed exists + parent = this.root + for (let pathIndex = 0; pathIndex < path.length - 1; pathIndex++) + parent = parent[path[pathIndex]] + } - this.path = path - for (const morph of morphs) { - const result = morph( - parent === undefined ? this.root : parent[key!], - this - ) - if (result instanceof ArkErrors) return result - if (this.hasError()) return this.errors - if (result instanceof ArkError) { - // if an ArkError was returned but wasn't added to these - // errors, add it then return - this.error(result) - return this.errors - } - - // apply the morph function and assign the result to the - // corresponding property, or to root if path is empty - if (parent === undefined) this.root = result - else parent[key!] = result + this.path = path + for (const morph of morphs) { + const result = morph( + parent === undefined ? this.root : parent[key!], + this + ) + if (result instanceof ArkErrors) return result + if (this.hasError()) return this.errors + if (result instanceof ArkError) { + // if an ArkError was returned but wasn't added to these + // errors, add it then return + this.error(result) + return this.errors } + + // apply the morph function and assign the result to the + // corresponding property, or to root if path is empty + if (parent === undefined) this.root = result + else parent[key!] = result } } return this.root diff --git a/ark/type/CHANGELOG.md b/ark/type/CHANGELOG.md index 321e84cbf5..9f4fac5d68 100644 --- a/ark/type/CHANGELOG.md +++ b/ark/type/CHANGELOG.md @@ -1,5 +1,46 @@ # arktype +## 2.0.0-dev.16 + +- Fix an incorrect return value on pipe sequences like the following: + +```ts +const Amount = type( + "string", + ":", + (s, ctx) => Number.isInteger(Number(s)) || ctx.invalid("number") +) + .pipe((s, ctx) => { + try { + return BigInt(s) + } catch { + return ctx.error("a non-decimal number") + } + }) + .narrow((amount, ctx) => true) + +const Token = type("7 s.toLowerCase()) + .narrow((s, ctx) => true) + +const $ = scope({ + Asset: { + token: Token, + amount: Amount + }, + Assets: () => $.type("Asset[]>=1").pipe(assets => assets) +}) + +const types = $.export() + +// now correctly returns { token: "lovelace", amount: 5000000n } +const out = types.Assets([{ token: "lovelace", amount: "5000000" }]) + +// (was previously { token: undefined, amount: undefined }) +``` + +https://github.com/arktypeio/arktype/pull/974 + ## 2.0.0-dev.15 - Fix a crash when piping to nested paths (see https://github.com/arktypeio/arktype/issues/968) diff --git a/ark/type/__tests__/pipe.test.ts b/ark/type/__tests__/pipe.test.ts index d24daa6bd0..14f06e85f9 100644 --- a/ark/type/__tests__/pipe.test.ts +++ b/ark/type/__tests__/pipe.test.ts @@ -78,6 +78,12 @@ contextualize(() => { attest(t.json).equals(expected.json) }) + it("disjoint", () => { + attest(() => type("number>5").pipe(type("number<3"))).throws.snap( + "ParseError: Intersection of <3 and >5 results in an unsatisfiable type" + ) + }) + it("uses pipe for many consecutive types", () => { const t = type({ a: "1" }).pipe( type({ b: "1" }), diff --git a/ark/type/__tests__/realWorld.test.ts b/ark/type/__tests__/realWorld.test.ts index dd1bcb0238..b5a2022b9b 100644 --- a/ark/type/__tests__/realWorld.test.ts +++ b/ark/type/__tests__/realWorld.test.ts @@ -395,4 +395,39 @@ nospace must be matched by ^\\S*$ (was "One space")`) attest(out).snap({ assets: { a: "1n" } }) }) + + // https://discord.com/channels/957797212103016458/957804102685982740/1243850690644934677 + it("more chained pipes/narrows", () => { + const Amount = type( + "string", + ":", + (s, ctx) => Number.isInteger(Number(s)) || ctx.invalid("number") + ) + .pipe((s, ctx) => { + try { + return BigInt(s) + } catch { + return ctx.error("a non-decimal number") + } + }) + .narrow((amount, ctx) => true) + + const Token = type("7 s.toLowerCase()) + .narrow((s, ctx) => true) + + const $ = scope({ + Asset: { + token: Token, + amount: Amount + }, + Assets: () => $.type("Asset[]>=1").pipe(assets => assets) + }) + + const types = $.export() + + const out = types.Assets([{ token: "lovelace", amount: "5000000" }]) + + attest(out).snap([{ token: "lovelace", amount: "5000000n" }]) + }) }) diff --git a/ark/type/package.json b/ark/type/package.json index 8e044b5df1..fb5ddd7c5a 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.15", + "version": "2.0.0-dev.16", "license": "MIT", "author": { "name": "David Blass",