From 5a325f1951eb5b4bdc622c94161e3fae81010fcf Mon Sep 17 00:00:00 2001 From: Dimava Date: Mon, 28 Oct 2024 22:23:29 +0300 Subject: [PATCH] fix: disable pipes on both children and parent properties --- ark/repo/scratch.ts | 25 +++++++++++++++-- ark/schema/shared/errors.ts | 19 ++++++++++++- ark/schema/shared/traversal.ts | 4 +++ ark/type/__tests__/pipe.test.ts | 48 +++++++++++++++++++++++++++++++++ 4 files changed, 93 insertions(+), 3 deletions(-) diff --git a/ark/repo/scratch.ts b/ark/repo/scratch.ts index 6f3f86224d..27016f4aac 100644 --- a/ark/repo/scratch.ts +++ b/ark/repo/scratch.ts @@ -1,5 +1,26 @@ import { type } from "arktype" -const t = type({ - foo: "string" +const $Item = type({ + type: "string", + "additionalProperties?": "this", + "description?": "string", + "enum?": "string[]", + "example?": "number|string|object|boolean", + "items?": "this", + "required?": "string[]" }) + +const DescribedEnum = type({ + branches: "string", // type({ unit: "string" }).array().atLeastLength(1), + meta: "string" +}).pipe((v, ctx) => { + console.log(ctx.currentErrorCount, Object.keys(ctx.errors.byPath)) + console.log("DescribedEnum is expected to be a valid type, but is: ", v) + return { + // ... + } +}, $Item) +console.clear() +console.log(JSON.stringify(DescribedEnum.json)) +const v = DescribedEnum({ domain: "number", meta: "A whole number;example:0" }) +console.log(v + "") diff --git a/ark/schema/shared/errors.ts b/ark/schema/shared/errors.ts index 707e878d68..90a3c3151a 100644 --- a/ark/schema/shared/errors.ts +++ b/ark/schema/shared/errors.ts @@ -10,7 +10,12 @@ import type { Prerequisite, errorContext } from "../kinds.ts" import type { NodeKind } from "./implement.ts" import type { StandardSchema } from "./standardSchema.ts" import type { TraversalContext } from "./traversal.ts" -import { arkKind, pathToPropString, type TraversalPath } from "./utils.ts" +import { + appendPropToPathString, + arkKind, + pathToPropString, + type TraversalPath +} from "./utils.ts" export type ArkErrorResult = ArkError | ArkErrors @@ -87,6 +92,7 @@ export class ArkErrors } byPath: Record = Object.create(null) + ignorePaths: Record = Object.create(null) count = 0 private mutable: ArkError[] = this as never @@ -118,6 +124,17 @@ export class ArkErrors } else { this.byPath[error.propString] = error this.mutable.push(error) + + // add superpaths of the error path to ignored + let partialPropString: string = "" + this.ignorePaths[partialPropString] = true + for (let i = 0; i < error.path.length; i++) { + partialPropString = appendPropToPathString( + partialPropString, + error.path[i] + ) + this.ignorePaths[partialPropString] = true + } } this.count++ } diff --git a/ark/schema/shared/traversal.ts b/ark/schema/shared/traversal.ts index 41861c63eb..3488c3394a 100644 --- a/ark/schema/shared/traversal.ts +++ b/ark/schema/shared/traversal.ts @@ -149,6 +149,7 @@ export class TraversalContext { pathHasError(path: TraversalPath): boolean { if (!this.hasError()) return false + // Check super-paths of the error path let partialPropString: string = "" // this.errors.byPath is null prototyped so indexing by string is safe if (this.errors.byPath[partialPropString]) return true @@ -156,6 +157,9 @@ export class TraversalContext { partialPropString = appendPropToPathString(partialPropString, path[i]) if (this.errors.byPath[partialPropString]) return true } + // Check sub-paths of the error path + if (this.errors.ignorePaths[partialPropString]) return true + return false } diff --git a/ark/type/__tests__/pipe.test.ts b/ark/type/__tests__/pipe.test.ts index d56cc16f30..0e8405b3cf 100644 --- a/ark/type/__tests__/pipe.test.ts +++ b/ark/type/__tests__/pipe.test.ts @@ -318,6 +318,54 @@ contextualize(() => { attest(t({ a: 2 })).snap(3) }) + it("doesn't pipe in child property error", () => { + let calls = 0 + const a = type({ a: "number" }).pipe( + () => { + calls++ + return { a: "string" } + }, + type({ a: "string" }) + ) + attest(a({ a: 1 })).snap({ a: "string" }) + attest(calls).snap(1) + attest(a({ a: "error" }).toString()).snap( + "a must be a number (was a string)" + ) + attest(calls).snap(1) + }) + it("doesn't pipe in parent property error", () => { + let calls = 0 + const a = type({ a: "number" }).pipe( + (v, ctx) => { + if (v.a !== 1) ctx.mustBe("{ a: 1 }") + return { a: "string" } + }, + type({ + a: type("string").pipe(() => calls++) + }) + ) + attest(a({ a: 1 })).snap({ a: 0 }) + attest(calls).snap(1) + attest(a({ a: 2 }).toString()).snap('must be { a: 1 } (was {"a":2})') + attest(calls).snap(1) + }) + it("doesn't pipe child property of parent piped property error", () => { + let calls = 0 + const a = type({ a: "number" }).pipe( + () => ({ a: "string" }), + type({ + a: type("string").pipe(() => calls++) + }) + ) + attest(a({ a: 1 })).snap({ a: 0 }) + attest(calls).snap(1) + attest(a({ a: "error" }).toString()).snap( + "a must be a number (was a string)" + ) + attest(calls).snap(1) + }) + it("in array", () => { const types = scope({ lengthOfString: ["string", "=>", data => data.length],