Skip to content

Commit

Permalink
feat: options.complexityThreshold
Browse files Browse the repository at this point in the history
  • Loading branch information
astahmer committed Oct 25, 2022
1 parent ed3e320 commit dd361cc
Show file tree
Hide file tree
Showing 20 changed files with 665 additions and 72 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ const endpoints = makeApi([
schema: z.number().optional(),
},
],
response: Pets,
response: z.array(Pet),
},
{
method: "post",
Expand Down
5 changes: 5 additions & 0 deletions src/CodeMeta.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ReferenceObject, SchemaObject } from "openapi3-ts";
import { isReferenceObject } from "openapi3-ts";

import { getSchemaComplexity } from "./schema-complexity";
import { getRefName } from "./utils";

export type ConversionTypeContext = {
Expand Down Expand Up @@ -51,6 +52,10 @@ export class CodeMeta {
return getRefName(this.ref!);
}

get complexity(): number {
return getSchemaComplexity({ current: 0, schema: this.schema });
}

assign(code: string) {
this.code = code;

Expand Down
5 changes: 5 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ cli.command("<input>", "path/url to OpenAPI/Swagger document as json/yaml")
"--group-strategy",
"groups endpoints by a given strategy, possible values are: 'none' | 'tag' | 'method' | 'tag-file' | 'method-file'"
)
.option(
"--complexity-threshold",
"schema complexity threshold to determine which one (using less than `<` operator) should be assigned to a variable"
)
.action(async (input, options) => {
console.log("Retrieving OpenAPI document from", input);
const openApiDoc = (await SwaggerParser.bundle(input)) as OpenAPIObject;
Expand All @@ -55,6 +59,7 @@ cli.command("<input>", "path/url to OpenAPI/Swagger document as json/yaml")
withImplicitRequiredProps: options.implicitRequired,
withDeprecatedEndpoints: options.withDeprecated,
groupStrategy: options.groupStrategy,
complexityThreshold: options.complexityThreshold,
},
});
console.log(`Done generating <${distPath}> !`);
Expand Down
36 changes: 12 additions & 24 deletions src/generateZodClientFromOpenAPI.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ test("getZodClientTemplateContext", async () => {
"parameters": [
{
"name": "status",
"schema": "status",
"schema": "z.enum(["available", "pending", "sold"]).optional()",
"type": "Query",
},
],
Expand All @@ -155,7 +155,7 @@ test("getZodClientTemplateContext", async () => {
"parameters": [
{
"name": "tags",
"schema": "tags",
"schema": "z.array(z.string()).optional()",
"type": "Query",
},
],
Expand Down Expand Up @@ -298,7 +298,7 @@ test("getZodClientTemplateContext", async () => {
{
"description": undefined,
"name": "body",
"schema": "createUsersWithListInput_Body",
"schema": "z.array(User)",
"type": "Body",
},
],
Expand Down Expand Up @@ -356,9 +356,6 @@ test("getZodClientTemplateContext", async () => {
"Pet": "z.object({ id: z.number().int().optional(), name: z.string(), category: Category.optional(), photoUrls: z.array(z.string()), tags: z.array(Tag).optional(), status: z.enum(["available", "pending", "sold"]).optional() })",
"Tag": "z.object({ id: z.number().int(), name: z.string() }).partial()",
"User": "z.object({ id: z.number().int(), username: z.string(), firstName: z.string(), lastName: z.string(), email: z.string(), password: z.string(), phone: z.string(), userStatus: z.number().int() }).partial()",
"createUsersWithListInput_Body": "z.array(User)",
"status": "z.enum(["available", "pending", "sold"]).optional()",
"tags": "z.array(z.string()).optional()",
},
"types": {},
}
Expand All @@ -382,8 +379,6 @@ describe("generateZodClientFromOpenAPI", () => {
tags: z.array(Tag).optional(),
status: z.enum(["available", "pending", "sold"]).optional(),
});
const status = z.enum(["available", "pending", "sold"]).optional();
const tags = z.array(z.string()).optional();
const ApiResponse = z
.object({ code: z.number().int(), type: z.string(), message: z.string() })
.partial();
Expand All @@ -409,7 +404,6 @@ describe("generateZodClientFromOpenAPI", () => {
userStatus: z.number().int(),
})
.partial();
const createUsersWithListInput_Body = z.array(User);
const endpoints = makeApi([
{
Expand Down Expand Up @@ -519,7 +513,7 @@ describe("generateZodClientFromOpenAPI", () => {
{
name: "status",
type: "Query",
schema: status,
schema: z.enum(["available", "pending", "sold"]).optional(),
},
],
response: z.array(Pet),
Expand All @@ -540,7 +534,7 @@ describe("generateZodClientFromOpenAPI", () => {
{
name: "tags",
type: "Query",
schema: tags,
schema: z.array(z.string()).optional(),
},
],
response: z.array(Pet),
Expand Down Expand Up @@ -675,7 +669,7 @@ describe("generateZodClientFromOpenAPI", () => {
{
name: "body",
type: "Body",
schema: createUsersWithListInput_Body,
schema: z.array(User),
},
],
response: User,
Expand Down Expand Up @@ -738,8 +732,6 @@ describe("generateZodClientFromOpenAPI", () => {
tags: z.array(Tag).optional(),
status: z.enum(["available", "pending", "sold"]).optional(),
});
const status = z.enum(["available", "pending", "sold"]).optional();
const tags = z.array(z.string()).optional();
const ApiResponse = z
.object({ code: z.number().int(), type: z.string(), message: z.string() })
.partial();
Expand All @@ -765,7 +757,6 @@ describe("generateZodClientFromOpenAPI", () => {
userStatus: z.number().int(),
})
.partial();
const createUsersWithListInput_Body = z.array(User);
const endpoints = makeApi([
{
Expand Down Expand Up @@ -880,7 +871,7 @@ describe("generateZodClientFromOpenAPI", () => {
{
name: "status",
type: "Query",
schema: status,
schema: z.enum(["available", "pending", "sold"]).optional(),
},
],
response: z.array(Pet),
Expand All @@ -902,7 +893,7 @@ describe("generateZodClientFromOpenAPI", () => {
{
name: "tags",
type: "Query",
schema: tags,
schema: z.array(z.string()).optional(),
},
],
response: z.array(Pet),
Expand Down Expand Up @@ -1044,7 +1035,7 @@ describe("generateZodClientFromOpenAPI", () => {
{
name: "body",
type: "Body",
schema: createUsersWithListInput_Body,
schema: z.array(User),
},
],
response: User,
Expand Down Expand Up @@ -1111,8 +1102,6 @@ describe("generateZodClientFromOpenAPI", () => {
tags: z.array(Tag).optional(),
status: z.enum(["available", "pending", "sold"]).optional(),
});
const status = z.enum(["available", "pending", "sold"]).optional();
const tags = z.array(z.string()).optional();
const ApiResponse = z
.object({ code: z.number().int(), type: z.string(), message: z.string() })
.partial();
Expand All @@ -1138,7 +1127,6 @@ describe("generateZodClientFromOpenAPI", () => {
userStatus: z.number().int(),
})
.partial();
const createUsersWithListInput_Body = z.array(User);
const endpoints = makeApi([
{
Expand Down Expand Up @@ -1248,7 +1236,7 @@ describe("generateZodClientFromOpenAPI", () => {
{
name: "status",
type: "Query",
schema: status,
schema: z.enum(["available", "pending", "sold"]).optional(),
},
],
response: z.array(Pet),
Expand All @@ -1269,7 +1257,7 @@ describe("generateZodClientFromOpenAPI", () => {
{
name: "tags",
type: "Query",
schema: tags,
schema: z.array(z.string()).optional(),
},
],
response: z.array(Pet),
Expand Down Expand Up @@ -1404,7 +1392,7 @@ describe("generateZodClientFromOpenAPI", () => {
{
name: "body",
type: "Body",
schema: createUsersWithListInput_Body,
schema: z.array(User),
},
],
response: User,
Expand Down
26 changes: 7 additions & 19 deletions src/getZodiosEndpointDefinitionList.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,7 @@ test("getZodiosEndpointDefinitionFromOpenApiDoc /pet/findXXX", () => {
"parameters": [
{
"name": "status",
"schema": "status",
"schema": "z.enum(["available", "pending", "sold"]).optional()",
"type": "Query",
},
],
Expand All @@ -440,7 +440,7 @@ test("getZodiosEndpointDefinitionFromOpenApiDoc /pet/findXXX", () => {
"parameters": [
{
"name": "tags",
"schema": "tags",
"schema": "z.array(z.string()).optional()",
"type": "Query",
},
],
Expand All @@ -456,16 +456,11 @@ test("getZodiosEndpointDefinitionFromOpenApiDoc /pet/findXXX", () => {
"#/components/schemas/Tag",
},
},
"schemaByName": {
"z.array(z.string()).optional()": "tags",
"z.enum(["available", "pending", "sold"]).optional()": "status",
},
"schemaByName": {},
"zodSchemaByName": {
"Category": "z.object({ id: z.number().int(), name: z.string() }).partial()",
"Pet": "z.object({ id: z.number().int().optional(), name: z.string(), category: Category.optional(), photoUrls: z.array(z.string()), tags: z.array(Tag).optional(), status: z.enum(["available", "pending", "sold"]).optional() })",
"Tag": "z.object({ id: z.number().int(), name: z.string() }).partial()",
"status": "z.enum(["available", "pending", "sold"]).optional()",
"tags": "z.array(z.string()).optional()",
},
}
`);
Expand Down Expand Up @@ -553,7 +548,7 @@ test("petstore.yaml", async () => {
"parameters": [
{
"name": "status",
"schema": "status",
"schema": "z.enum(["available", "pending", "sold"]).optional()",
"type": "Query",
},
],
Expand All @@ -575,7 +570,7 @@ test("petstore.yaml", async () => {
"parameters": [
{
"name": "tags",
"schema": "tags",
"schema": "z.array(z.string()).optional()",
"type": "Query",
},
],
Expand Down Expand Up @@ -803,7 +798,7 @@ test("petstore.yaml", async () => {
{
"description": undefined,
"name": "body",
"schema": "createUsersWithListInput_Body",
"schema": "z.array(User)",
"type": "Body",
},
],
Expand Down Expand Up @@ -932,21 +927,14 @@ test("petstore.yaml", async () => {
"#/components/schemas/Tag",
},
},
"schemaByName": {
"z.array(User)": "createUsersWithListInput_Body",
"z.array(z.string()).optional()": "tags",
"z.enum(["available", "pending", "sold"]).optional()": "status",
},
"schemaByName": {},
"zodSchemaByName": {
"ApiResponse": "z.object({ code: z.number().int(), type: z.string(), message: z.string() }).partial()",
"Category": "z.object({ id: z.number().int(), name: z.string() }).partial()",
"Order": "z.object({ id: z.number().int(), petId: z.number().int(), quantity: z.number().int(), shipDate: z.string(), status: z.enum(["placed", "approved", "delivered"]), complete: z.boolean() }).partial()",
"Pet": "z.object({ id: z.number().int().optional(), name: z.string(), category: Category.optional(), photoUrls: z.array(z.string()), tags: z.array(Tag).optional(), status: z.enum(["available", "pending", "sold"]).optional() })",
"Tag": "z.object({ id: z.number().int(), name: z.string() }).partial()",
"User": "z.object({ id: z.number().int(), username: z.string(), firstName: z.string(), lastName: z.string(), email: z.string(), password: z.string(), phone: z.string(), userStatus: z.number().int() }).partial()",
"createUsersWithListInput_Body": "z.array(User)",
"status": "z.enum(["available", "pending", "sold"]).optional()",
"tags": "z.array(z.string()).optional()",
},
}
`);
Expand Down
23 changes: 17 additions & 6 deletions src/getZodiosEndpointDefinitionList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { sync } from "whence";
import type { CodeMeta, ConversionTypeContext } from "./CodeMeta";
import { getOpenApiDependencyGraph } from "./getOpenApiDependencyGraph";
import { getZodChainablePresence, getZodSchema } from "./openApiToZod";
import { getSchemaComplexity } from "./schema-complexity";
import type { TemplateContext } from "./template-context";
import { getRefFromName, normalizeString, pathToVariableName } from "./utils";

Expand Down Expand Up @@ -53,19 +54,24 @@ export const getZodiosEndpointDefinitionList = (doc: OpenAPIObject, options?: Te
}

const ctx: ConversionTypeContext = { getSchemaByRef, zodSchemaByName: {}, schemaByName: {} };
const complexityThreshold = options?.complexityThreshold ?? 4;
const getZodVarName = (input: CodeMeta, fallbackName?: string) => {
const result = input.toString();

// special value, inline everything (= no variable used)
if (complexityThreshold === -1) {
return input.ref ? ctx.zodSchemaByName[result]! : result;
}

if (result.startsWith("z.") && fallbackName) {
// result is simple enough that it doesn't need to be assigned to a variable
if (!complexType.some((type) => result.startsWith(type))) {
// if (input.complexity < 10) {
if (input.complexity < complexityThreshold) {
return result;
}

const safeName = normalizeString(fallbackName);

// if schema is already assigned to a variable, re-use that variable
// if schema is already assigned to a variable, re-use that variable name
if (ctx.schemaByName[result]) {
return ctx.schemaByName[result]!;
}
Expand All @@ -87,7 +93,14 @@ export const getZodiosEndpointDefinitionList = (doc: OpenAPIObject, options?: Te
}

// result is a reference to another schema
if (ctx.zodSchemaByName[result]) {
if (input.ref && ctx.zodSchemaByName[result]) {
const complexity = getSchemaComplexity({ current: 0, schema: getSchemaByRef(input.ref) });

// ref result is simple enough that it doesn't need to be assigned to a variable
if (complexity < complexityThreshold) {
return ctx.zodSchemaByName[result]!;
}

return result;
}

Expand Down Expand Up @@ -186,7 +199,6 @@ export const getZodiosEndpointDefinitionList = (doc: OpenAPIObject, options?: Te

if (isMainResponseStatus(status) || (statusCode === "default" && !endpointDescription.response)) {
endpointDescription.response = schemaString;
// console.log({ schema, schemaString });

if (
!endpointDescription.description &&
Expand Down Expand Up @@ -235,5 +247,4 @@ export type EndpointDescriptionWithRefs = Omit<
errors: Array<Omit<Required<ZodiosEndpointDefinition<any>>["errors"][number], "schema"> & { schema: string }>;
};

const complexType = ["z.object", "z.array", "z.union", "z.enum"] as const;
const pathParamRegex = /{(\w+)}/g;
Loading

0 comments on commit dd361cc

Please sign in to comment.