Skip to content

Commit 3aa9162

Browse files
committedApr 4, 2023
fix(zod): handling correctly deeper ref by deferencing
1 parent d952146 commit 3aa9162

File tree

7 files changed

+184
-65
lines changed

7 files changed

+184
-65
lines changed
 

‎packages/core/src/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -716,6 +716,7 @@ export type WriteModeProps = {
716716
workspace: string;
717717
specsName: Record<string, string>;
718718
header: string;
719+
needSchema: boolean;
719720
};
720721

721722
export type GeneratorApiOperations = {

‎packages/core/src/writers/single-mode.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export const writeSingleMode = async ({
1414
output,
1515
specsName,
1616
header,
17+
needSchema,
1718
}: WriteModeProps): Promise<string[]> => {
1819
try {
1920
const { path, dirname } = getFileInfo(output.target, {
@@ -89,7 +90,7 @@ export const writeSingleMode = async ({
8990
data += generateMutatorImports({ mutators: formUrlEncoded });
9091
}
9192

92-
if (!output.schemas) {
93+
if (!output.schemas && needSchema) {
9394
data += generateModelsInline(builder.schemas);
9495
}
9596

‎packages/core/src/writers/split-mode.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export const writeSplitMode = async ({
1414
output,
1515
specsName,
1616
header,
17+
needSchema,
1718
}: WriteModeProps): Promise<string[]> => {
1819
try {
1920
const { filename, dirname, extension } = getFileInfo(output.target, {
@@ -69,7 +70,7 @@ export const writeSplitMode = async ({
6970
? upath.join(dirname, filename + '.schemas' + extension)
7071
: undefined;
7172

72-
if (schemasPath) {
73+
if (schemasPath && needSchema) {
7374
const schemasData = header + generateModelsInline(builder.schemas);
7475

7576
await fs.outputFile(

‎packages/core/src/writers/split-tags-mode.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export const writeSplitTagsMode = async ({
1414
output,
1515
specsName,
1616
header,
17+
needSchema,
1718
}: WriteModeProps): Promise<string[]> => {
1819
const { filename, dirname, extension } = getFileInfo(output.target, {
1920
backupFilename: camel(builder.info.title),
@@ -74,7 +75,7 @@ export const writeSplitTagsMode = async ({
7475
? upath.join(dirname, filename + '.schemas' + extension)
7576
: undefined;
7677

77-
if (schemasPath) {
78+
if (schemasPath && needSchema) {
7879
const schemasData = header + generateModelsInline(builder.schemas);
7980

8081
await fs.outputFile(schemasPath, schemasData);

‎packages/core/src/writers/tags-mode.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export const writeTagsMode = async ({
1515
output,
1616
specsName,
1717
header,
18+
needSchema,
1819
}: WriteModeProps): Promise<string[]> => {
1920
const { filename, dirname, extension } = getFileInfo(output.target, {
2021
backupFilename: camel(builder.info.title),
@@ -78,7 +79,7 @@ export const writeTagsMode = async ({
7879
? upath.join(dirname, filename + '.schemas' + extension)
7980
: undefined;
8081

81-
if (schemasPath) {
82+
if (schemasPath && needSchema) {
8283
const schemasData = header + generateModelsInline(builder.schemas);
8384

8485
await fs.outputFile(schemasPath, schemasData);

‎packages/orval/src/write-specs.ts

+1
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export const writeSpecs = async (
8787
output,
8888
specsName,
8989
header,
90+
needSchema: !output.schemas && output.client !== 'zod',
9091
});
9192
}
9293

‎packages/zod/src/index.ts

+174-61
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@ import {
1616
GeneratorVerbOptions,
1717
isString,
1818
resolveRef,
19+
ContextSpecs,
20+
isObject,
21+
isBoolean,
1922
} from '@orval/core';
23+
import SwaggerParser from '@apidevtools/swagger-parser';
2024

2125
const ZOD_DEPENDENCIES: GeneratorDependency[] = [
2226
{
@@ -42,7 +46,7 @@ const resolveZodType = (schemaTypeValue: SchemaObject['type']) => {
4246
case 'null':
4347
return 'mixed';
4448
default:
45-
return schemaTypeValue ?? 'mixed';
49+
return schemaTypeValue ?? 'any';
4650
}
4751
};
4852

@@ -55,85 +59,124 @@ const generateZodValidationSchemaDefinition = (
5559

5660
const consts = [];
5761
const functions: [string, any][] = [];
58-
const type = resolveZodType(schema?.type);
62+
const type = resolveZodType(schema.type);
5963
const required =
60-
schema?.default !== undefined
64+
schema.default !== undefined
6165
? false
62-
: _required ?? !schema?.nullable ?? false;
66+
: _required ?? !schema.nullable ?? false;
6367
const min =
64-
schema?.minimum ??
65-
schema?.exclusiveMinimum ??
66-
schema?.minLength ??
67-
undefined;
68+
schema.minimum ?? schema.exclusiveMinimum ?? schema.minLength ?? undefined;
6869
const max =
69-
schema?.maximum ??
70-
schema?.exclusiveMaximum ??
71-
schema?.maxLength ??
72-
undefined;
73-
const matches = schema?.pattern ?? undefined;
70+
schema.maximum ?? schema.exclusiveMaximum ?? schema.maxLength ?? undefined;
71+
const matches = schema.pattern ?? undefined;
7472

7573
switch (type) {
76-
case 'object':
77-
functions.push([
78-
'object',
79-
Object.keys(schema?.properties ?? {})
80-
.map((key) => ({
81-
[key]: generateZodValidationSchemaDefinition(
82-
schema?.properties?.[key] as any,
83-
schema?.required?.includes(key),
84-
camel(`${name}-${key}`),
85-
),
86-
}))
87-
.reduce((acc, curr) => ({ ...acc, ...curr }), {}),
88-
]);
89-
break;
9074
case 'array':
91-
const items = schema?.items as SchemaObject | undefined;
75+
const items = schema.items as SchemaObject | undefined;
9276
functions.push([
9377
'array',
9478
generateZodValidationSchemaDefinition(items, true, camel(name)),
9579
]);
9680
break;
9781
case 'string': {
98-
if (schema?.enum && type === 'string') {
82+
if (schema.enum && type === 'string') {
9983
break;
10084
}
10185

10286
functions.push([type as string, undefined]);
10387

104-
if (schema?.format === 'date-time' || schema?.format === 'date') {
88+
if (schema.format === 'date-time' || schema.format === 'date') {
10589
functions.push(['datetime', undefined]);
10690
break;
10791
}
10892

109-
if (schema?.format === 'email') {
93+
if (schema.format === 'email') {
11094
functions.push(['email', undefined]);
11195
break;
11296
}
11397

114-
if (schema?.format === 'uri' || schema?.format === 'hostname') {
98+
if (schema.format === 'uri' || schema.format === 'hostname') {
11599
functions.push(['url', undefined]);
116100
break;
117101
}
118102

119-
if (schema?.format === 'uuid') {
103+
if (schema.format === 'uuid') {
120104
functions.push(['uuid', undefined]);
121105
break;
122106
}
123107

124108
break;
125109
}
126-
default:
110+
case 'object':
111+
default: {
112+
if (schema.allOf || schema.oneOf || schema.anyOf) {
113+
const separator = schema.allOf
114+
? 'allOf'
115+
: schema.oneOf
116+
? 'oneOf'
117+
: 'anyOf';
118+
119+
const schemas = (schema.allOf ?? schema.oneOf ?? schema.anyOf) as (
120+
| SchemaObject
121+
| ReferenceObject
122+
)[];
123+
124+
functions.push([
125+
separator,
126+
schemas.map((schema) =>
127+
generateZodValidationSchemaDefinition(
128+
schema as SchemaObject,
129+
true,
130+
camel(name),
131+
),
132+
),
133+
]);
134+
break;
135+
}
136+
137+
if (schema.properties) {
138+
functions.push([
139+
'object',
140+
Object.keys(schema.properties)
141+
.map((key) => ({
142+
[key]: generateZodValidationSchemaDefinition(
143+
schema.properties?.[key] as any,
144+
schema.required?.includes(key),
145+
camel(`${name}-${key}`),
146+
),
147+
}))
148+
.reduce((acc, curr) => ({ ...acc, ...curr }), {}),
149+
]);
150+
151+
break;
152+
}
153+
154+
if (schema.additionalProperties) {
155+
functions.push([
156+
'additionalProperties',
157+
isBoolean(schema.additionalProperties)
158+
? schema.additionalProperties
159+
: generateZodValidationSchemaDefinition(
160+
schema.additionalProperties as SchemaObject,
161+
true,
162+
name,
163+
),
164+
]);
165+
166+
break;
167+
}
168+
127169
functions.push([type as string, undefined]);
128170
break;
171+
}
129172
}
130173

131174
if (min !== undefined) {
132175
consts.push(`export const ${name}Min = ${min};`);
133176
functions.push(['min', `${name}Min`]);
134177
}
135178
if (max !== undefined) {
136-
consts.push(`export const ${name}Max = ${min};`);
179+
consts.push(`export const ${name}Max = ${max};`);
137180
functions.push(['max', `${name}Max`]);
138181
}
139182
if (matches) {
@@ -147,11 +190,12 @@ const generateZodValidationSchemaDefinition = (
147190
consts.push(`export const ${name}RegExp = ${regexp};`);
148191
functions.push(['regex', `${name}RegExp`]);
149192
}
150-
if (schema?.enum) {
193+
194+
if (schema.enum && type !== 'number') {
151195
functions.push([
152196
'enum',
153197
[
154-
`[${schema?.enum
198+
`[${schema.enum
155199
.map((value) => (isString(value) ? `'${escape(value)}'` : `${value}`))
156200
.join(', ')}]`,
157201
],
@@ -168,40 +212,109 @@ const generateZodValidationSchemaDefinition = (
168212
const parseZodValidationSchemaDefinition = (
169213
input: Record<string, { functions: [string, any][]; consts: string[] }>,
170214
): { zod: string; consts: string } => {
171-
const parseProperty = ([fn, args = '']: [string, any]): string => {
172-
if (fn === 'object') return ` ${parseZodValidationSchemaDefinition(args)}`;
173-
if (fn === 'array')
174-
return `.array(${
175-
Array.isArray(args)
176-
? `zod${args.map(parseProperty).join('')}`
177-
: parseProperty(args)
178-
})`;
179-
180-
return `.${fn}(${args})`;
181-
};
182-
183215
if (!Object.keys(input).length) {
184216
return { zod: '', consts: '' };
185217
}
186218

187-
const consts = Object.entries(input).reduce((acc, [key, schema]) => {
219+
let consts = '';
220+
221+
const parseProperty = (property: [string, any]): string => {
222+
const [fn, args = ''] = property;
223+
if (fn === 'allOf') {
224+
return args.reduce(
225+
(acc: string, { functions }: { functions: [string, any][] }) => {
226+
const value = functions.map(parseProperty).join('');
227+
const valueWithZod = `${value.startsWith('.') ? 'zod' : ''}${value}`;
228+
229+
if (!acc) {
230+
acc += valueWithZod;
231+
return acc;
232+
}
233+
234+
acc += `.and(${valueWithZod})`;
235+
236+
return acc;
237+
},
238+
'',
239+
);
240+
}
241+
242+
if (fn === 'oneOf' || fn === 'anyOf') {
243+
return args.reduce(
244+
(acc: string, { functions }: { functions: [string, any][] }) => {
245+
const value = functions.map(parseProperty).join('');
246+
const valueWithZod = `${value.startsWith('.') ? 'zod' : ''}${value}`;
247+
248+
if (!acc) {
249+
acc += valueWithZod;
250+
return acc;
251+
}
252+
253+
acc += `.or(${valueWithZod})`;
254+
255+
return acc;
256+
},
257+
'',
258+
);
259+
}
260+
261+
if (fn === 'additionalProperties') {
262+
const value = args.functions.map(parseProperty).join('');
263+
const valueWithZod = `${value.startsWith('.') ? 'zod' : ''}${value}`;
264+
return `zod.record(zod.string(), ${valueWithZod})`;
265+
}
266+
267+
if (fn === 'object') {
268+
const parsed = parseZodValidationSchemaDefinition(args);
269+
consts += parsed.consts;
270+
return ` ${parsed.zod}`;
271+
}
272+
if (fn === 'array') {
273+
const value = args.functions.map(parseProperty).join('');
274+
return `.array(${value.startsWith('.') ? 'zod' : ''}${value})`;
275+
}
276+
return `.${fn}(${args})`;
277+
};
278+
279+
consts += Object.entries(input).reduce((acc, [key, schema]) => {
188280
return acc + schema.consts.join('\n');
189281
}, '');
190282

191283
const zod = `zod.object({
192284
${Object.entries(input)
193-
.map(
194-
([key, schema]) =>
195-
`"${key}": ${
196-
schema.functions[0][0] !== 'object' ? 'zod' : ''
197-
}${schema.functions.map(parseProperty).join('')}`,
198-
)
285+
.map(([key, schema]) => {
286+
const value = schema.functions.map(parseProperty).join('');
287+
return `"${key}": ${value.startsWith('.') ? 'zod' : ''}${value}`;
288+
})
199289
.join(',')}
200290
})`;
201291

202292
return { zod, consts };
203293
};
204294

295+
const deferenceScalar = (value: any, context: ContextSpecs): unknown => {
296+
if (isObject(value)) {
297+
return deference(value, context);
298+
} else if (Array.isArray(value)) {
299+
return value.map((item) => deferenceScalar(item, context));
300+
} else {
301+
return value;
302+
}
303+
};
304+
305+
const deference = (
306+
schema: SchemaObject | ReferenceObject,
307+
context: ContextSpecs,
308+
): SchemaObject => {
309+
const { schema: resolvedSchema } = resolveRef<SchemaObject>(schema, context);
310+
311+
return Object.entries(resolvedSchema).reduce((acc, [key, value]) => {
312+
acc[key] = deferenceScalar(value, context);
313+
314+
return acc;
315+
}, {} as any);
316+
};
317+
205318
const generateZodRoute = (
206319
{ operationName, body, verb }: GeneratorVerbOptions,
207320
{ pathRoute, context }: GeneratorOptions,
@@ -231,13 +344,13 @@ const generateZodRoute = (
231344

232345
const zodDefinitionsResponseProperties =
233346
resolvedResponseJsonSchema?.properties ??
234-
([] as (SchemaObject | ReferenceObject)[]);
347+
({} as { [p: string]: SchemaObject | ReferenceObject });
235348

236349
const zodDefinitionsResponse = Object.entries(
237350
zodDefinitionsResponseProperties,
238351
)
239352
.map(([key, response]) => {
240-
const { schema } = resolveRef<SchemaObject>(response, context);
353+
const schema = deference(response, context);
241354

242355
return {
243356
[key]: generateZodValidationSchemaDefinition(
@@ -266,11 +379,11 @@ const generateZodRoute = (
266379

267380
const zodDefinitionsBodyProperties =
268381
resolvedRequestBodyJsonSchema?.properties ??
269-
([] as (SchemaObject | ReferenceObject)[]);
382+
({} as { [p: string]: SchemaObject | ReferenceObject });
270383

271384
const zodDefinitionsBody = Object.entries(zodDefinitionsBodyProperties)
272385
.map(([key, body]) => {
273-
const { schema } = resolveRef<SchemaObject>(body, context);
386+
const schema = deference(body, context);
274387

275388
return {
276389
[key]: generateZodValidationSchemaDefinition(
@@ -292,7 +405,7 @@ const generateZodRoute = (
292405
return acc;
293406
}
294407

295-
const { schema } = resolveRef<SchemaObject>(parameter.schema, context);
408+
const schema = deference(parameter.schema, context);
296409

297410
const definition = generateZodValidationSchemaDefinition(
298411
schema,

1 commit comments

Comments
 (1)

vercel[bot] commented on Apr 4, 2023

@vercel[bot]
Please sign in to comment.