Skip to content

Commit

Permalink
feat: add support for multiple interface extensions (#68)
Browse files Browse the repository at this point in the history
* feat: add support for multiple interface extensions

* fix: add extension dependencies
  • Loading branch information
tvillaren committed Feb 3, 2022
1 parent b5043eb commit e349c33
Show file tree
Hide file tree
Showing 2 changed files with 116 additions and 45 deletions.
46 changes: 36 additions & 10 deletions src/core/generateZodSchema.test.ts
Expand Up @@ -253,16 +253,6 @@ describe("generateZodSchema", () => {
);
});

it("should throw on not supported interface extends", () => {
const source = `export interface Superman extends Clark extends KalL {
withCap: true;
};`;

expect(() => generate(source)).toThrowErrorMatchingInlineSnapshot(
`"Only interface with single \`extends T\` are not supported!"`
);
});

it("should throw on not supported interface with extends and index signature", () => {
const source = `export interface Superman extends Clark {
[key: string]: any;
Expand Down Expand Up @@ -373,6 +363,42 @@ describe("generateZodSchema", () => {
`);
});

it("should generate a merged schema when two extends are used", () => {
const source = `export interface Superman extends Clark extends KalL {
withPower: boolean;
};`;

expect(generate(source)).toMatchInlineSnapshot(`
"export const supermanSchema = clarkSchema.extend(kalLSchema.shape).extend({
withPower: z.boolean()
});"
`);
});

it("should generate a merged schema when extending with two comma-separated interfaces", () => {
const source = `export interface Superman extends Clark, KalL {
withPower: boolean;
};`;

expect(generate(source)).toMatchInlineSnapshot(`
"export const supermanSchema = clarkSchema.extend(kalLSchema.shape).extend({
withPower: z.boolean()
});"
`);
});

it("should generate a merged schema when extending with multiple comma-separated interfaces", () => {
const source = `export interface Superman extends Clark, KalL, Kryptonian {
withPower: boolean;
};`;

expect(generate(source)).toMatchInlineSnapshot(`
"export const supermanSchema = clarkSchema.extend(kalLSchema.shape).extend(kryptonianSchema.shape).extend({
withPower: z.boolean()
});"
`);
});

it("should deal with literal keys", () => {
const source = `export interface Villain {
"i.will.kill.everybody": true;
Expand Down
115 changes: 80 additions & 35 deletions src/core/generateZodSchema.ts
Expand Up @@ -70,33 +70,42 @@ export function generateZodSchemaVariableStatement({
| ts.Identifier
| ts.PropertyAccessExpression
| undefined;
const dependencies: string[] = [];
let dependencies: string[] = [];
let requiresImport = false;

if (ts.isInterfaceDeclaration(node)) {
let baseSchema: string | undefined;
let schemaExtensionClauses: string[] | undefined;
if (node.typeParameters) {
throw new Error("Interface with generics are not supported!");
}
if (node.heritageClauses) {
if (
node.heritageClauses.length > 1 ||
node.heritageClauses[0].types.length > 1
) {
throw new Error(
"Only interface with single `extends T` are not supported!"
);
}
const type = node.heritageClauses[0].types[0];
baseSchema = getDependencyName(type.expression.getText(sourceFile));
// Looping on heritageClauses browses the "extends" keywords
schemaExtensionClauses = node.heritageClauses.reduce(
(deps: string[], h) => {
if (h.token !== ts.SyntaxKind.ExtendsKeyword || !h.types) {
return deps;
}

// Looping on types browses the comma-separated interfaces
const heritages = h.types.map((expression) => {
return getDependencyName(expression.getText(sourceFile));
});

return deps.concat(heritages);
},
[]
);

dependencies = dependencies.concat(schemaExtensionClauses);
}

schema = buildZodObject({
typeNode: node,
sourceFile,
z: zodImportValue,
dependencies,
getDependencyName,
baseSchema,
schemaExtensionClauses,
skipParseJSDoc,
});
}
Expand Down Expand Up @@ -417,8 +426,9 @@ function buildZodPrimitive({

const dependencyName = getDependencyName(identifierName);
dependencies.push(dependencyName);
const zodSchema: ts.Identifier | ts.CallExpression =
f.createIdentifier(dependencyName);
const zodSchema: ts.Identifier | ts.CallExpression = f.createIdentifier(
dependencyName
);
return withZodProperties(zodSchema, zodProperties);
}

Expand Down Expand Up @@ -727,6 +737,35 @@ function buildZodSchema(
return withZodProperties(zodCall, properties);
}

function buildZodExtendedSchema(
schemaList: string[],
args?: ts.Expression[],
properties?: ZodProperty[]
) {
let zodCall = f.createIdentifier(schemaList[0]) as ts.Expression;

for (let i = 1; i < schemaList.length; i++) {
zodCall = f.createCallExpression(
f.createPropertyAccessExpression(zodCall, f.createIdentifier("extend")),
undefined,
[
f.createPropertyAccessExpression(
f.createIdentifier(schemaList[i]),
f.createIdentifier("shape")
),
]
);
}

zodCall = f.createCallExpression(
f.createPropertyAccessExpression(zodCall, f.createIdentifier("extend")),
undefined,
args
);

return withZodProperties(zodCall, properties);
}

/**
* Apply zod properties to an expression (as `.optional()`)
*
Expand Down Expand Up @@ -760,15 +799,15 @@ function buildZodObject({
dependencies,
sourceFile,
getDependencyName,
baseSchema,
schemaExtensionClauses,
skipParseJSDoc,
}: {
typeNode: ts.TypeLiteralNode | ts.InterfaceDeclaration;
z: string;
dependencies: string[];
sourceFile: ts.SourceFile;
getDependencyName: Required<GenerateZodSchemaProps>["getDependencyName"];
baseSchema?: string;
schemaExtensionClauses?: string[];
skipParseJSDoc: boolean;
}) {
const { properties, indexSignature } = typeNode.members.reduce<{
Expand Down Expand Up @@ -805,22 +844,29 @@ function buildZodObject({
skipParseJSDoc,
});

objectSchema = buildZodSchema(
baseSchema || z,
baseSchema ? "extend" : "object",
[
if (schemaExtensionClauses && schemaExtensionClauses.length > 0) {
objectSchema = buildZodExtendedSchema(schemaExtensionClauses, [
f.createObjectLiteralExpression(
Array.from(parsedProperties.entries()).map(([key, tsCall]) => {
return f.createPropertyAssignment(key, tsCall);
}),
true
),
]
);
]);
} else {
objectSchema = buildZodSchema(z, "object", [
f.createObjectLiteralExpression(
Array.from(parsedProperties.entries()).map(([key, tsCall]) => {
return f.createPropertyAssignment(key, tsCall);
}),
true
),
]);
}
}

if (indexSignature) {
if (baseSchema) {
if (schemaExtensionClauses) {
throw new Error(
"interface with `extends` and index signature are not supported!"
);
Expand Down Expand Up @@ -882,17 +928,16 @@ function buildSchemaReference(

if (indexTypeName === "-1") {
// Get the original type declaration
const declaration = findNode(
sourceFile,
(n): n is ts.InterfaceDeclaration | ts.TypeAliasDeclaration => {
return (
(ts.isInterfaceDeclaration(n) || ts.isTypeAliasDeclaration(n)) &&
ts.isIndexedAccessTypeNode(node.objectType) &&
n.name.getText(sourceFile) ===
node.objectType.objectType.getText(sourceFile).split("[")[0]
);
}
);
const declaration = findNode(sourceFile, (n): n is
| ts.InterfaceDeclaration
| ts.TypeAliasDeclaration => {
return (
(ts.isInterfaceDeclaration(n) || ts.isTypeAliasDeclaration(n)) &&
ts.isIndexedAccessTypeNode(node.objectType) &&
n.name.getText(sourceFile) ===
node.objectType.objectType.getText(sourceFile).split("[")[0]
);
});

if (declaration && ts.isIndexedAccessTypeNode(node.objectType)) {
const key = node.objectType.indexType.getText(sourceFile).slice(1, -1); // remove quotes
Expand Down

0 comments on commit e349c33

Please sign in to comment.