Skip to content

Commit 775aa65

Browse files
committed
feat: enhance generic step to support recursive structures and improve factory function handling
1 parent c04e475 commit 775aa65

File tree

2 files changed

+65
-3
lines changed

2 files changed

+65
-3
lines changed

packages/internal/src/steps/generic/generic.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,36 @@ describe('generic plugin', () => {
6363
const result = schema.execute('world')
6464
expect(result).toEqual({ value: 'world' })
6565
})
66+
67+
it('should handle recursive structures using generic', () => {
68+
interface MyNode {
69+
id: number
70+
children?: MyNode[]
71+
}
72+
73+
const nodeSchema = v.object({
74+
id: v.number(),
75+
// Required to use a factory function with specifying return type `any` to avoid circular type reference.
76+
children: [v.array(v.generic<{ output: MyNode }>((): any => nodeSchema))],
77+
})
78+
79+
const result = nodeSchema.execute({
80+
id: 1,
81+
children: [
82+
{ id: 2 },
83+
{ id: 3, children: [{ id: 4 }] },
84+
],
85+
})
86+
expect(result).toEqual({
87+
value: {
88+
id: 1,
89+
children: [
90+
{ id: 2 },
91+
{ id: 3, children: [{ id: 4 }] },
92+
],
93+
},
94+
})
95+
})
6696
})
6797

6898
describe('integration with other steps', () => {

packages/internal/src/steps/generic/generic.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,41 @@ export const generic = implStepPlugin<PluginDef>({
6161
utils: { addStep },
6262
params: [stepOrFactory],
6363
}) => {
64-
const step = typeof stepOrFactory === 'function' ? stepOrFactory() : stepOrFactory
65-
for (const s of step['~core'].runtimeSteps) {
66-
addStep(s)
64+
// Handle factory function
65+
if (typeof stepOrFactory === 'function') {
66+
addStep((lastResult) => {
67+
const runtimeSteps = stepOrFactory()['~core'].runtimeSteps
68+
const len = runtimeSteps.length
69+
let result: any = lastResult
70+
let isAsync = false
71+
72+
for (let i = 0; i < len; i++) {
73+
if (isAsync) {
74+
// Already in async mode, skip synchronous execution
75+
continue
76+
}
77+
// Execute step synchronously
78+
result = runtimeSteps[i]!(result)
79+
// Check if current result is a promise
80+
if (result instanceof Promise) {
81+
isAsync = true
82+
// Once we hit async, chain all remaining steps
83+
for (let j = i + 1; j < len; j++) {
84+
result = result.then(runtimeSteps[j]!)
85+
}
86+
return result
87+
}
88+
}
89+
return result
90+
})
91+
}
92+
// Handle direct step
93+
else {
94+
const runtimeSteps = stepOrFactory['~core'].runtimeSteps
95+
const len = runtimeSteps.length
96+
for (let i = 0; i < len; i++) {
97+
addStep(runtimeSteps[i]!)
98+
}
6799
}
68100
},
69101
})

0 commit comments

Comments
 (0)