Skip to content

Commit 2de015e

Browse files
committed
feat(zod): oneOf directive handling
1 parent e7aca20 commit 2de015e

File tree

2 files changed

+949
-25
lines changed

2 files changed

+949
-25
lines changed

src/zod/index.ts

Lines changed: 107 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -45,31 +45,30 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor {
4545

4646
initialEmit(): string {
4747
return (
48-
`\n${
49-
[
50-
new DeclarationBlock({})
51-
.asKind('type')
52-
.withName('Properties<T>')
53-
.withContent(['Required<{', ' [K in keyof T]: z.ZodType<T[K], any, T[K]>;', '}>'].join('\n'))
54-
.string,
55-
// Unfortunately, zod doesn’t provide non-null defined any schema.
56-
// This is a temporary hack until it is fixed.
57-
// see: https://github.com/colinhacks/zod/issues/884
58-
new DeclarationBlock({}).asKind('type').withName('definedNonNullAny').withContent('{}').string,
59-
new DeclarationBlock({})
60-
.export()
61-
.asKind('const')
62-
.withName(`isDefinedNonNullAny`)
63-
.withContent(`(v: any): v is definedNonNullAny => v !== undefined && v !== null`)
64-
.string,
65-
new DeclarationBlock({})
66-
.export()
67-
.asKind('const')
68-
.withName(`${anySchema}`)
69-
.withContent(`z.any().refine((v) => isDefinedNonNullAny(v))`)
70-
.string,
71-
...this.enumDeclarations,
72-
].join('\n')}`
48+
`\n${[
49+
new DeclarationBlock({})
50+
.asKind('type')
51+
.withName('Properties<T>')
52+
.withContent(['Required<{', ' [K in keyof T]: z.ZodType<T[K], any, T[K]>;', '}>'].join('\n'))
53+
.string,
54+
// Unfortunately, zod doesn’t provide non-null defined any schema.
55+
// This is a temporary hack until it is fixed.
56+
// see: https://github.com/colinhacks/zod/issues/884
57+
new DeclarationBlock({}).asKind('type').withName('definedNonNullAny').withContent('{}').string,
58+
new DeclarationBlock({})
59+
.export()
60+
.asKind('const')
61+
.withName(`isDefinedNonNullAny`)
62+
.withContent(`(v: any): v is definedNonNullAny => v !== undefined && v !== null`)
63+
.string,
64+
new DeclarationBlock({})
65+
.export()
66+
.asKind('const')
67+
.withName(`${anySchema}`)
68+
.withContent(`z.any().refine((v) => isDefinedNonNullAny(v))`)
69+
.string,
70+
...this.enumDeclarations,
71+
].join('\n')}`
7372
);
7473
}
7574

@@ -79,6 +78,13 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor {
7978
const visitor = this.createVisitor('input');
8079
const name = visitor.convertName(node.name.value);
8180
this.importTypes.push(name);
81+
82+
const hasOneOf = node.directives?.some(directive => directive?.name.value === 'oneOf');
83+
if (hasOneOf) {
84+
const result = this.buildOneOfInputFields(node.fields ?? [], visitor, name)
85+
return result;
86+
}
87+
8288
return this.buildInputFields(node.fields ?? [], visitor, name);
8389
},
8490
};
@@ -276,13 +282,89 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor {
276282
.string;
277283
}
278284
}
285+
286+
protected buildOneOfInputFields(
287+
fields: readonly (FieldDefinitionNode | InputValueDefinitionNode)[],
288+
visitor: Visitor,
289+
name: string,
290+
) {
291+
// FIXME: If I end up adding an explicit z.ZodUnion return type, make sure to handle schemaNamespacedImportName
292+
// Also type prefix and suffix
293+
294+
const typeName = visitor.prefixTypeNamespace(name);
295+
const union = generateFieldUnionZodSchema(this.config, visitor, fields, 2)
296+
297+
switch (this.config.validationSchemaExportType) {
298+
case 'const':
299+
return new DeclarationBlock({})
300+
.export()
301+
.asKind('const')
302+
.withName(`${typeName}Schema`)
303+
.withContent(['z.union([', union, '])'].join('\n'))
304+
.string;
305+
306+
case 'function':
307+
default:
308+
return new DeclarationBlock({})
309+
.export()
310+
.asKind('function')
311+
.withName(`${name}Schema(): z.ZodUnion<z.ZodUnionOptions>`)
312+
.withBlock([indent(`return z.union([`), `${union}`, indent('])')].join('\n'))
313+
.string;
314+
}
315+
}
279316
}
280317

281318
function generateFieldZodSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, indentCount: number): string {
282319
const gen = generateFieldTypeZodSchema(config, visitor, field, field.type);
283320
return indent(`${field.name.value}: ${maybeLazy(visitor, field.type, gen)}`, indentCount);
284321
}
285322

323+
function generateFieldUnionZodSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, fields: readonly (FieldDefinitionNode | InputValueDefinitionNode)[], indentCount: number): string {
324+
const union = fields.map((field) => {
325+
const objectFields = fields.map((nestedObjectField) => {
326+
const isSameField = field.name === nestedObjectField.name;
327+
328+
if (!isSameField) {
329+
return indent(`${nestedObjectField.name.value}: z.never()`, indentCount + 1);
330+
}
331+
332+
if (isListType(nestedObjectField.type)) {
333+
const gen = generateFieldTypeZodSchema(config, visitor, nestedObjectField, nestedObjectField.type.type, nestedObjectField.type);
334+
335+
const maybeLazyGen = `z.array(${maybeLazy(visitor, nestedObjectField.type.type, gen)})`;
336+
const appliedDirectivesGen = applyDirectives(config, nestedObjectField, maybeLazyGen);
337+
338+
return indent(`${nestedObjectField.name.value}: ${appliedDirectivesGen}`, 3);
339+
}
340+
341+
if (isNonNullType(nestedObjectField.type)) {
342+
const gen = generateFieldTypeZodSchema(config, visitor, field, nestedObjectField.type.type, nestedObjectField.type);
343+
344+
return indent(`${nestedObjectField.name.value}: ${maybeLazy(visitor, nestedObjectField.type.type, gen)}`, indentCount + 1);
345+
}
346+
347+
if (isNamedType(nestedObjectField.type)) {
348+
const gen = generateNameNodeZodSchema(config, visitor, nestedObjectField.type.name);
349+
350+
const appliedDirectivesGen = applyDirectives(config, nestedObjectField, gen);
351+
352+
if (visitor.shouldEmitAsNotAllowEmptyString(nestedObjectField.type.name.value))
353+
return indent(`${nestedObjectField.name.value}: ${maybeLazy(visitor, nestedObjectField.type, appliedDirectivesGen)}.min(1)`, indentCount + 1);
354+
355+
return indent(`${nestedObjectField.name.value}: ${maybeLazy(visitor, nestedObjectField.type, appliedDirectivesGen)}`, indentCount + 1);
356+
}
357+
358+
console.warn('unhandled type:', field.type);
359+
return '';
360+
});
361+
362+
return [indent('z.object({', indentCount), objectFields.join(',\n'), indent('})', indentCount)].join('\n');
363+
}).join(',\n');
364+
365+
return union;
366+
}
367+
286368
function generateFieldTypeZodSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, type: TypeNode, parentType?: TypeNode): string {
287369
if (isListType(type)) {
288370
const gen = generateFieldTypeZodSchema(config, visitor, field, type.type, type);

0 commit comments

Comments
 (0)