diff --git a/package.json b/package.json index b8996cb..48f5e0b 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "locate-character": "^3.0.0", "openai": "^4.98.0", "yaml": "^2.4.5", - "zod": "^3.23.8" + "zod": "^4.1.8" }, "devDependencies": { "@eslint/eslintrc": "^3.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a790190..5640d0b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,13 +22,13 @@ importers: version: 3.0.0 openai: specifier: ^4.98.0 - version: 4.98.0(zod@3.23.8) + version: 4.98.0(zod@4.1.11) yaml: specifier: ^2.4.5 version: 2.6.1 zod: - specifier: ^3.23.8 - version: 3.23.8 + specifier: ^4.1.8 + version: 4.1.11 devDependencies: '@eslint/eslintrc': specifier: ^3.2.0 @@ -1570,8 +1570,8 @@ packages: resolution: {integrity: sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==} engines: {node: '>=12.20'} - zod@3.23.8: - resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} + zod@4.1.11: + resolution: {integrity: sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==} snapshots: @@ -2598,7 +2598,7 @@ snapshots: dependencies: mimic-fn: 4.0.0 - openai@4.98.0(zod@3.23.8): + openai@4.98.0(zod@4.1.11): dependencies: '@types/node': 18.19.100 '@types/node-fetch': 2.6.12 @@ -2608,7 +2608,7 @@ snapshots: formdata-node: 4.4.1 node-fetch: 2.7.0 optionalDependencies: - zod: 3.23.8 + zod: 4.1.11 transitivePeerDependencies: - encoding @@ -2912,4 +2912,4 @@ snapshots: yocto-queue@1.1.1: {} - zod@3.23.8: {} + zod@4.1.11: {} diff --git a/src/compiler/scan.ts b/src/compiler/scan.ts index bb94a5e..e2b673d 100644 --- a/src/compiler/scan.ts +++ b/src/compiler/scan.ts @@ -319,8 +319,8 @@ export class Scan { this.configSchema?.parse(parsedObj) } catch (err) { if (isZodError(err)) { - err.errors.forEach((error) => { - const { message, path } = getMostSpecificError(error) + err.issues.forEach((issue) => { + const { message, path } = getMostSpecificError(issue) const range = findYAMLItemPosition( parsedYaml.contents as YAMLItem, diff --git a/src/compiler/utils.test.ts b/src/compiler/utils.test.ts index 33286c3..8fa96f0 100644 --- a/src/compiler/utils.test.ts +++ b/src/compiler/utils.test.ts @@ -1,9 +1,10 @@ import { describe, it, expect } from 'vitest' -import { ZodError, ZodIssue, ZodIssueCode, z } from 'zod' +import { ZodError, core, z } from 'zod' import { getMostSpecificError } from './utils' +type ZodIssue = core.$ZodIssue + function makeZodError(issues: ZodIssue[]): ZodError { - // @ts-ignore return new ZodError(issues) } @@ -11,9 +12,9 @@ describe('getMostSpecificError', () => { it('returns the message and path for a simple error', () => { const error = makeZodError([ { - code: ZodIssueCode.invalid_type, + code: 'invalid_type', expected: 'string', - received: 'number', + input: 'number', path: ['foo'], message: 'Expected string', }, @@ -26,26 +27,26 @@ describe('getMostSpecificError', () => { it('returns the most specific (deepest) error in a nested structure', () => { const unionError = makeZodError([ { - code: ZodIssueCode.invalid_union, - unionErrors: [ - makeZodError([ + code: 'invalid_union', + errors: [ + [ { - code: ZodIssueCode.invalid_type, + code: 'invalid_type', expected: 'string', - received: 'number', + input: 'number', path: ['foo', 'bar'], message: 'Expected string', }, - ]), - makeZodError([ + ], + [ { - code: ZodIssueCode.invalid_type, + code: 'invalid_type', expected: 'number', - received: 'string', + input: 'string', path: ['foo'], message: 'Expected number', }, - ]), + ], ], path: ['foo'], message: 'Invalid union', @@ -59,7 +60,7 @@ describe('getMostSpecificError', () => { it('returns the error message and empty path if no issues', () => { const error = makeZodError([ { - code: ZodIssueCode.custom, + code: 'custom', path: [], message: 'Custom error', }, @@ -72,16 +73,16 @@ describe('getMostSpecificError', () => { it('handles errors with multiple paths and picks the deepest', () => { const error = makeZodError([ { - code: ZodIssueCode.invalid_type, + code: 'invalid_type', expected: 'string', - received: 'number', + input: 'number', path: ['a'], message: 'Expected string', }, { - code: ZodIssueCode.invalid_type, + code: 'invalid_type', expected: 'number', - received: 'string', + input: 'string', path: ['a', 'b', 'c'], message: 'Expected number', }, diff --git a/src/compiler/utils.ts b/src/compiler/utils.ts index caf15d6..5c6529c 100644 --- a/src/compiler/utils.ts +++ b/src/compiler/utils.ts @@ -1,3 +1,4 @@ +import { ZodError, core } from 'zod' import { TAG_NAMES } from '$promptl/constants' import { ChainStepTag, @@ -9,7 +10,8 @@ import { } from '$promptl/parser/interfaces' import { ContentTypeTagName, MessageRole } from '$promptl/types' import { Scalar, Node as YAMLItem, YAMLMap, YAMLSeq } from 'yaml' -import { ZodError, ZodIssue, ZodIssueCode } from 'zod' + +type ZodIssue = core.$ZodIssue export function isIterable(obj: unknown): obj is Iterable { return (obj as Iterable)?.[Symbol.iterator] !== undefined @@ -76,7 +78,7 @@ export function tagAttributeIsLiteral(tag: ElementTag, name: string): boolean { type YAMLItemRange = [number, number] | undefined export function findYAMLItemPosition( parent: YAMLItem, - path: (string | number)[], + path: PropertyKey[], ): YAMLItemRange { const parentRange: YAMLItemRange = parent?.range ? [parent.range[0], parent.range[1]] @@ -101,145 +103,78 @@ export function findYAMLItemPosition( } export function isZodError(error: unknown): error is ZodError { - if (!(error instanceof Error)) return false - - if (error instanceof ZodError) return true - if (error.constructor.name === 'ZodError') return true - if ('issues' in error && error.issues instanceof Array) return true - - return false + return error instanceof ZodError } function collectAllLeafIssues(issue: ZodIssue): ZodIssue[] { + if (issue.code === 'invalid_union') { + issue.errors + return issue.errors.flatMap((issues) => + issues.flatMap(collectAllLeafIssues), + ) + } + return [issue] +} + +function getZodIssueMessage(issue: ZodIssue): string { switch (issue.code) { - case ZodIssueCode.invalid_union: { - // invalid_union.issue.unionErrors is ZodError[] - const unionErrs: ZodError[] = (issue as any).unionErrors ?? [] - return unionErrs.flatMap((nestedZodError) => - nestedZodError.issues.flatMap((nestedIssue) => - collectAllLeafIssues(nestedIssue), - ), - ) + case 'invalid_type': { + const attr = issue.path.at(-1) + return typeof attr === 'string' + ? `Expected type \`${issue.expected}\` for attribute "${attr}", but received \`${issue.input}\`.` + : `Expected type \`${issue.expected}\`, but received \`${issue.input}\`.` } - case ZodIssueCode.invalid_arguments: { - // invalid_arguments.issue.argumentsError is ZodError - const argsErr: ZodError | undefined = (issue as any).argumentsError - if (argsErr) { - return argsErr.issues.flatMap((nestedIssue) => - collectAllLeafIssues(nestedIssue), - ) - } - return [issue] + case 'invalid_value': { + const attr = issue.path.at(-1) + const literalValues = issue.values.join(', ') + return typeof attr === 'string' + ? `Expected literal \`${literalValues}\` for attribute "${attr}", but received \`${issue.input}\`.` + : `Expected literal \`${literalValues}\`, but received \`${issue.input}\`.` } - case ZodIssueCode.invalid_return_type: { - // invalid_return_type.issue.returnTypeError is ZodError - const retErr: ZodError | undefined = (issue as any).returnTypeError - if (retErr) { - return retErr.issues.flatMap((nestedIssue) => - collectAllLeafIssues(nestedIssue), - ) - } - return [issue] - } + case 'unrecognized_keys': + return `Unrecognized keys: ${issue.keys.join(', ')}.` - default: - // Any other issue code is considered a “leaf” (no deeper nested ZodError) - return [issue] - } -} + case 'invalid_union': + return `Invalid union: ${issue.message}` -function getZodIssueMessage(issue: ZodIssue): string { - if (issue.code === ZodIssueCode.invalid_type) { - const attribute = issue.path[issue.path.length - 1] - if (typeof attribute === 'string') { - return `Expected type \`${issue.expected}\` for attribute "${attribute}", but received \`${issue.received}\`.` - } + case 'too_small': + return `Value is too small: ${issue.message || 'Value does not meet minimum size.'}` - return `Expected type \`${issue.expected}\`, but received \`${issue.received}\`.` - } - if (issue.code === ZodIssueCode.invalid_literal) { - const attribute = issue.path[issue.path.length - 1] - if (typeof attribute === 'string') { - return `Expected literal \`${issue.expected}\` for attribute "${attribute}", but received \`${issue.received}\`.` - } + case 'too_big': + return `Value is too big: ${issue.message || 'Value exceeds maximum size.'}` - return `Expected literal \`${issue.expected}\`, but received \`${issue.received}\`.` - } - if (issue.code === ZodIssueCode.unrecognized_keys) { - return `Unrecognized keys: ${issue.keys.join(', ')}.` - } - if (issue.code === ZodIssueCode.invalid_union) { - return `Invalid union: ${issue.unionErrors - .map((err) => err.message) - .join(', ')}` - } - if (issue.code === ZodIssueCode.invalid_union_discriminator) { - return `Invalid union discriminator. Expected one of: ${issue.options.join( - ', ', - )}.` - } - if (issue.code === ZodIssueCode.invalid_enum_value) { - return `Invalid enum value: ${issue.received}. Expected one of: ${issue.options.join( - ', ', - )}.` - } - if (issue.code === ZodIssueCode.invalid_arguments) { - return `Invalid arguments: ${issue.argumentsError.issues - .map((err) => err.message) - .join(', ')}` - } - if (issue.code === ZodIssueCode.invalid_return_type) { - return `Invalid return type: ${issue.returnTypeError.issues - .map((err) => err.message) - .join(', ')}` - } - if (issue.code === ZodIssueCode.invalid_date) { - return `Invalid date: ${issue.message || 'Invalid date format.'}` - } - if (issue.code === ZodIssueCode.invalid_string) { - return `Invalid string: ${issue.message || 'String does not match expected format.'}` - } - if (issue.code === ZodIssueCode.too_small) { - return `Value is too small: ${issue.message || 'Value does not meet minimum size.'}` - } - if (issue.code === ZodIssueCode.too_big) { - return `Value is too big: ${issue.message || 'Value exceeds maximum size.'}` - } - if (issue.code === ZodIssueCode.invalid_intersection_types) { - return `Invalid intersection types: ${issue.message || 'Types do not match.'}` - } - if (issue.code === ZodIssueCode.not_multiple_of) { - return `Value is not a multiple of ${issue.multipleOf}: ${issue.message || 'Value does not meet multiple of condition.'}` - } - if (issue.code === ZodIssueCode.not_finite) { - return `Value is not finite: ${issue.message || 'Value must be a finite number.'}` - } - if (issue.code === ZodIssueCode.custom) { - return `Custom validation error: ${issue.message || 'No additional message provided.'}` + case 'not_multiple_of': + return `Value is not a multiple of ${issue.divisor}: ${issue.message || 'Value does not meet multiple of condition.'}` + + case 'custom': + return `Custom validation error: ${issue.message || 'No additional message provided.'}` + + case 'invalid_key': + return `Invalid key: ${issue.message || 'Key validation failed.'}` + + case 'invalid_format': + return `Invalid format: ${issue.message || `Expected format ${issue.format}.`}` + + case 'invalid_element': + return `Invalid element: ${issue.message || 'Element validation failed.'}` + + default: + // The types are exhaustive, but we don't want to miss any new ones + return 'Unknown validation error.' } - // For any other issue code, return the message directly - return (issue as ZodIssue).message || 'Unknown validation error.' } export function getMostSpecificError(error: ZodIssue): { message: string - path: (string | number)[] + path: PropertyKey[] } { const allIssues = collectAllLeafIssues(error) - - if (allIssues.length === 0) { - return { message: error.message, path: [] } - } - - let mostSpecific = allIssues[0]! - for (const issue of allIssues) { - if (issue.path.length > mostSpecific.path.length) { - mostSpecific = issue - } - } - + const mostSpecific = allIssues.reduce( + (acc, cur) => (cur.path.length > acc.path.length ? cur : acc), + allIssues[0]!, + ) return { message: getZodIssueMessage(mostSpecific), path: mostSpecific.path,