diff --git a/ark/schema/shared/errors.ts b/ark/schema/shared/errors.ts index 0fe50527c..bc83d7a9b 100644 --- a/ark/schema/shared/errors.ts +++ b/ark/schema/shared/errors.ts @@ -82,7 +82,7 @@ export class ArkErrors extends ReadonlyArray { this.ctx = ctx } - byPath: Record = {} + byPath: Record = Object.create(null) count = 0 private mutable: ArkError[] = this as never diff --git a/ark/schema/shared/traversal.ts b/ark/schema/shared/traversal.ts index 2ba1b958e..41861c63e 100644 --- a/ark/schema/shared/traversal.ts +++ b/ark/schema/shared/traversal.ts @@ -8,7 +8,7 @@ import { type ArkErrorContextInput, type ArkErrorInput } from "./errors.ts" -import { isNode, pathToPropString, type TraversalPath } from "./utils.ts" +import { appendPropToPathString, isNode, type TraversalPath } from "./utils.ts" export type MorphsAtPath = { path: TraversalPath @@ -149,8 +149,14 @@ export class TraversalContext { pathHasError(path: TraversalPath): boolean { if (!this.hasError()) return false - const propString = pathToPropString(path) - return this.errors.some(e => propString.startsWith(e.propString)) + let partialPropString: string = "" + // this.errors.byPath is null prototyped so indexing by string is safe + if (this.errors.byPath[partialPropString]) return true + for (let i = 0; i < path.length; i++) { + partialPropString = appendPropToPathString(partialPropString, path[i]) + if (this.errors.byPath[partialPropString]) return true + } + return false } get failFast(): boolean { diff --git a/ark/schema/shared/utils.ts b/ark/schema/shared/utils.ts index ca2c47219..10d3d90f2 100644 --- a/ark/schema/shared/utils.ts +++ b/ark/schema/shared/utils.ts @@ -61,27 +61,49 @@ export type PathToPropStringFn = ( : NoInfer<[opts: PathToPropStringOptions]> ) => string -export const pathToPropString: PathToPropStringFn = (path, ...[opts]) => { +export type AppendPropToPathStringFn = ( + path: string, + prop: stringifiable, + ...[opts]: [stringifiable] extends [PropertyKey] ? + [opts?: PathToPropStringOptions] + : NoInfer<[opts: PathToPropStringOptions]> +) => string + +export const appendPropToPathString: AppendPropToPathStringFn = ( + path, + prop, + ...[opts] +) => { const stringifySymbol = opts?.stringifySymbol ?? printable - const propAccessChain = path.reduce((s, k) => { - switch (typeof k) { - case "string": - return isDotAccessible(k) ? `${s}.${k}` : `${s}[${JSON.stringify(k)}]` - case "number": - return `${s}[${k}]` - case "symbol": - return `${s}[${stringifySymbol(k)}]` - default: - if (opts?.stringifyNonKey) - return `${s}[${opts.stringifyNonKey(k as never)}]` - throwParseError( - `${printable(k)} must be a PropertyKey or stringifyNonKey must be passed to options` - ) - } - }, "") - return propAccessChain[0] === "." ? propAccessChain.slice(1) : propAccessChain + let propAccessChain: string = path + switch (typeof prop) { + case "string": + propAccessChain = + isDotAccessible(prop) ? + path === "" ? + prop + : `${path}.${prop}` + : `${path}[${JSON.stringify(prop)}]` + break + case "number": + propAccessChain = `${path}[${prop}]` + break + case "symbol": + propAccessChain = `${path}[${stringifySymbol(prop)}]` + break + default: + if (opts?.stringifyNonKey) + propAccessChain = `${path}[${opts.stringifyNonKey(prop as never)}]` + throwParseError( + `${printable(prop)} must be a PropertyKey or stringifyNonKey must be passed to options` + ) + } + return propAccessChain } +export const pathToPropString: PathToPropStringFn = (path, ...opts) => + path.reduce((s, k) => appendPropToPathString(s, k, ...opts), "") + export type arkKind = typeof arkKind export const arkKind = noSuggest("arkKind")