Skip to content

Commit efad8b6

Browse files
authored
Fix/loose config schema type (#42)
* Revert "Upgrade to from Zod v3 to Zod v4 (#41)" This reverts commit a8bc5bc. * Loose config schema type
1 parent 88833cc commit efad8b6

File tree

5 files changed

+153
-89
lines changed

5 files changed

+153
-89
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
"locate-character": "^3.0.0",
4444
"openai": "^4.98.0",
4545
"yaml": "^2.4.5",
46-
"zod": "^4.1.8"
46+
"zod": "^3.23.8"
4747
},
4848
"devDependencies": {
4949
"@eslint/eslintrc": "^3.2.0",

pnpm-lock.yaml

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/compiler/scan.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export class Scan {
5757
private fullPath: string
5858
private withParameters?: string[]
5959
private requireConfig: boolean
60-
private configSchema?: z.ZodType
60+
private configSchema?: z.ZodTypeAny
6161
private builtins: Record<string, () => any>
6262

6363
private config?: Config
@@ -319,8 +319,8 @@ export class Scan {
319319
this.configSchema?.parse(parsedObj)
320320
} catch (err) {
321321
if (isZodError(err)) {
322-
err.issues.forEach((issue) => {
323-
const { message, path } = getMostSpecificError(issue)
322+
err.errors.forEach((error) => {
323+
const { message, path } = getMostSpecificError(error)
324324

325325
const range = findYAMLItemPosition(
326326
parsedYaml.contents as YAMLItem,

src/compiler/utils.test.ts

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
11
import { describe, it, expect } from 'vitest'
2-
import { ZodError, core, z } from 'zod'
2+
import { ZodError, ZodIssue, ZodIssueCode, z } from 'zod'
33
import { getMostSpecificError } from './utils'
44

5-
type ZodIssue = core.$ZodIssue
6-
75
function makeZodError(issues: ZodIssue[]): ZodError {
6+
// @ts-ignore
87
return new ZodError(issues)
98
}
109

1110
describe('getMostSpecificError', () => {
1211
it('returns the message and path for a simple error', () => {
1312
const error = makeZodError([
1413
{
15-
code: 'invalid_type',
14+
code: ZodIssueCode.invalid_type,
1615
expected: 'string',
17-
input: 'number',
16+
received: 'number',
1817
path: ['foo'],
1918
message: 'Expected string',
2019
},
@@ -27,26 +26,26 @@ describe('getMostSpecificError', () => {
2726
it('returns the most specific (deepest) error in a nested structure', () => {
2827
const unionError = makeZodError([
2928
{
30-
code: 'invalid_union',
31-
errors: [
32-
[
29+
code: ZodIssueCode.invalid_union,
30+
unionErrors: [
31+
makeZodError([
3332
{
34-
code: 'invalid_type',
33+
code: ZodIssueCode.invalid_type,
3534
expected: 'string',
36-
input: 'number',
35+
received: 'number',
3736
path: ['foo', 'bar'],
3837
message: 'Expected string',
3938
},
40-
],
41-
[
39+
]),
40+
makeZodError([
4241
{
43-
code: 'invalid_type',
42+
code: ZodIssueCode.invalid_type,
4443
expected: 'number',
45-
input: 'string',
44+
received: 'string',
4645
path: ['foo'],
4746
message: 'Expected number',
4847
},
49-
],
48+
]),
5049
],
5150
path: ['foo'],
5251
message: 'Invalid union',
@@ -60,7 +59,7 @@ describe('getMostSpecificError', () => {
6059
it('returns the error message and empty path if no issues', () => {
6160
const error = makeZodError([
6261
{
63-
code: 'custom',
62+
code: ZodIssueCode.custom,
6463
path: [],
6564
message: 'Custom error',
6665
},
@@ -73,16 +72,16 @@ describe('getMostSpecificError', () => {
7372
it('handles errors with multiple paths and picks the deepest', () => {
7473
const error = makeZodError([
7574
{
76-
code: 'invalid_type',
75+
code: ZodIssueCode.invalid_type,
7776
expected: 'string',
78-
input: 'number',
77+
received: 'number',
7978
path: ['a'],
8079
message: 'Expected string',
8180
},
8281
{
83-
code: 'invalid_type',
82+
code: ZodIssueCode.invalid_type,
8483
expected: 'number',
85-
input: 'string',
84+
received: 'string',
8685
path: ['a', 'b', 'c'],
8786
message: 'Expected number',
8887
},

src/compiler/utils.ts

Lines changed: 122 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { ZodError, core } from 'zod'
21
import { TAG_NAMES } from '$promptl/constants'
32
import {
43
ChainStepTag,
@@ -10,8 +9,7 @@ import {
109
} from '$promptl/parser/interfaces'
1110
import { ContentTypeTagName, MessageRole } from '$promptl/types'
1211
import { Scalar, Node as YAMLItem, YAMLMap, YAMLSeq } from 'yaml'
13-
14-
type ZodIssue = core.$ZodIssue
12+
import { ZodError, ZodIssue, ZodIssueCode } from 'zod'
1513

1614
export function isIterable(obj: unknown): obj is Iterable<unknown> {
1715
return (obj as Iterable<unknown>)?.[Symbol.iterator] !== undefined
@@ -78,7 +76,7 @@ export function tagAttributeIsLiteral(tag: ElementTag, name: string): boolean {
7876
type YAMLItemRange = [number, number] | undefined
7977
export function findYAMLItemPosition(
8078
parent: YAMLItem,
81-
path: PropertyKey[],
79+
path: (string | number)[],
8280
): YAMLItemRange {
8381
const parentRange: YAMLItemRange = parent?.range
8482
? [parent.range[0], parent.range[1]]
@@ -103,78 +101,145 @@ export function findYAMLItemPosition(
103101
}
104102

105103
export function isZodError(error: unknown): error is ZodError {
106-
return error instanceof ZodError
107-
}
104+
if (!(error instanceof Error)) return false
108105

109-
function collectAllLeafIssues(issue: ZodIssue): ZodIssue[] {
110-
if (issue.code === 'invalid_union') {
111-
issue.errors
112-
return issue.errors.flatMap((issues) =>
113-
issues.flatMap(collectAllLeafIssues),
114-
)
115-
}
116-
return [issue]
106+
if (error instanceof ZodError) return true
107+
if (error.constructor.name === 'ZodError') return true
108+
if ('issues' in error && error.issues instanceof Array) return true
109+
110+
return false
117111
}
118112

119-
function getZodIssueMessage(issue: ZodIssue): string {
113+
function collectAllLeafIssues(issue: ZodIssue): ZodIssue[] {
120114
switch (issue.code) {
121-
case 'invalid_type': {
122-
const attr = issue.path.at(-1)
123-
return typeof attr === 'string'
124-
? `Expected type \`${issue.expected}\` for attribute "${attr}", but received \`${issue.input}\`.`
125-
: `Expected type \`${issue.expected}\`, but received \`${issue.input}\`.`
115+
case ZodIssueCode.invalid_union: {
116+
// invalid_union.issue.unionErrors is ZodError[]
117+
const unionErrs: ZodError[] = (issue as any).unionErrors ?? []
118+
return unionErrs.flatMap((nestedZodError) =>
119+
nestedZodError.issues.flatMap((nestedIssue) =>
120+
collectAllLeafIssues(nestedIssue),
121+
),
122+
)
126123
}
127124

128-
case 'invalid_value': {
129-
const attr = issue.path.at(-1)
130-
const literalValues = issue.values.join(', ')
131-
return typeof attr === 'string'
132-
? `Expected literal \`${literalValues}\` for attribute "${attr}", but received \`${issue.input}\`.`
133-
: `Expected literal \`${literalValues}\`, but received \`${issue.input}\`.`
125+
case ZodIssueCode.invalid_arguments: {
126+
// invalid_arguments.issue.argumentsError is ZodError
127+
const argsErr: ZodError | undefined = (issue as any).argumentsError
128+
if (argsErr) {
129+
return argsErr.issues.flatMap((nestedIssue) =>
130+
collectAllLeafIssues(nestedIssue),
131+
)
132+
}
133+
return [issue]
134134
}
135135

136-
case 'unrecognized_keys':
137-
return `Unrecognized keys: ${issue.keys.join(', ')}.`
138-
139-
case 'invalid_union':
140-
return `Invalid union: ${issue.message}`
141-
142-
case 'too_small':
143-
return `Value is too small: ${issue.message || 'Value does not meet minimum size.'}`
144-
145-
case 'too_big':
146-
return `Value is too big: ${issue.message || 'Value exceeds maximum size.'}`
147-
148-
case 'not_multiple_of':
149-
return `Value is not a multiple of ${issue.divisor}: ${issue.message || 'Value does not meet multiple of condition.'}`
150-
151-
case 'custom':
152-
return `Custom validation error: ${issue.message || 'No additional message provided.'}`
136+
case ZodIssueCode.invalid_return_type: {
137+
// invalid_return_type.issue.returnTypeError is ZodError
138+
const retErr: ZodError | undefined = (issue as any).returnTypeError
139+
if (retErr) {
140+
return retErr.issues.flatMap((nestedIssue) =>
141+
collectAllLeafIssues(nestedIssue),
142+
)
143+
}
144+
return [issue]
145+
}
153146

154-
case 'invalid_key':
155-
return `Invalid key: ${issue.message || 'Key validation failed.'}`
147+
default:
148+
// Any other issue code is considered a “leaf” (no deeper nested ZodError)
149+
return [issue]
150+
}
151+
}
156152

157-
case 'invalid_format':
158-
return `Invalid format: ${issue.message || `Expected format ${issue.format}.`}`
153+
function getZodIssueMessage(issue: ZodIssue): string {
154+
if (issue.code === ZodIssueCode.invalid_type) {
155+
const attribute = issue.path[issue.path.length - 1]
156+
if (typeof attribute === 'string') {
157+
return `Expected type \`${issue.expected}\` for attribute "${attribute}", but received \`${issue.received}\`.`
158+
}
159159

160-
case 'invalid_element':
161-
return `Invalid element: ${issue.message || 'Element validation failed.'}`
160+
return `Expected type \`${issue.expected}\`, but received \`${issue.received}\`.`
161+
}
162+
if (issue.code === ZodIssueCode.invalid_literal) {
163+
const attribute = issue.path[issue.path.length - 1]
164+
if (typeof attribute === 'string') {
165+
return `Expected literal \`${issue.expected}\` for attribute "${attribute}", but received \`${issue.received}\`.`
166+
}
162167

163-
default:
164-
// The types are exhaustive, but we don't want to miss any new ones
165-
return 'Unknown validation error.'
168+
return `Expected literal \`${issue.expected}\`, but received \`${issue.received}\`.`
166169
}
170+
if (issue.code === ZodIssueCode.unrecognized_keys) {
171+
return `Unrecognized keys: ${issue.keys.join(', ')}.`
172+
}
173+
if (issue.code === ZodIssueCode.invalid_union) {
174+
return `Invalid union: ${issue.unionErrors
175+
.map((err) => err.message)
176+
.join(', ')}`
177+
}
178+
if (issue.code === ZodIssueCode.invalid_union_discriminator) {
179+
return `Invalid union discriminator. Expected one of: ${issue.options.join(
180+
', ',
181+
)}.`
182+
}
183+
if (issue.code === ZodIssueCode.invalid_enum_value) {
184+
return `Invalid enum value: ${issue.received}. Expected one of: ${issue.options.join(
185+
', ',
186+
)}.`
187+
}
188+
if (issue.code === ZodIssueCode.invalid_arguments) {
189+
return `Invalid arguments: ${issue.argumentsError.issues
190+
.map((err) => err.message)
191+
.join(', ')}`
192+
}
193+
if (issue.code === ZodIssueCode.invalid_return_type) {
194+
return `Invalid return type: ${issue.returnTypeError.issues
195+
.map((err) => err.message)
196+
.join(', ')}`
197+
}
198+
if (issue.code === ZodIssueCode.invalid_date) {
199+
return `Invalid date: ${issue.message || 'Invalid date format.'}`
200+
}
201+
if (issue.code === ZodIssueCode.invalid_string) {
202+
return `Invalid string: ${issue.message || 'String does not match expected format.'}`
203+
}
204+
if (issue.code === ZodIssueCode.too_small) {
205+
return `Value is too small: ${issue.message || 'Value does not meet minimum size.'}`
206+
}
207+
if (issue.code === ZodIssueCode.too_big) {
208+
return `Value is too big: ${issue.message || 'Value exceeds maximum size.'}`
209+
}
210+
if (issue.code === ZodIssueCode.invalid_intersection_types) {
211+
return `Invalid intersection types: ${issue.message || 'Types do not match.'}`
212+
}
213+
if (issue.code === ZodIssueCode.not_multiple_of) {
214+
return `Value is not a multiple of ${issue.multipleOf}: ${issue.message || 'Value does not meet multiple of condition.'}`
215+
}
216+
if (issue.code === ZodIssueCode.not_finite) {
217+
return `Value is not finite: ${issue.message || 'Value must be a finite number.'}`
218+
}
219+
if (issue.code === ZodIssueCode.custom) {
220+
return `Custom validation error: ${issue.message || 'No additional message provided.'}`
221+
}
222+
// For any other issue code, return the message directly
223+
return (issue as ZodIssue).message || 'Unknown validation error.'
167224
}
168225

169226
export function getMostSpecificError(error: ZodIssue): {
170227
message: string
171-
path: PropertyKey[]
228+
path: (string | number)[]
172229
} {
173230
const allIssues = collectAllLeafIssues(error)
174-
const mostSpecific = allIssues.reduce(
175-
(acc, cur) => (cur.path.length > acc.path.length ? cur : acc),
176-
allIssues[0]!,
177-
)
231+
232+
if (allIssues.length === 0) {
233+
return { message: error.message, path: [] }
234+
}
235+
236+
let mostSpecific = allIssues[0]!
237+
for (const issue of allIssues) {
238+
if (issue.path.length > mostSpecific.path.length) {
239+
mostSpecific = issue
240+
}
241+
}
242+
178243
return {
179244
message: getZodIssueMessage(mostSpecific),
180245
path: mostSpecific.path,

0 commit comments

Comments
 (0)