From 01da570bf10c2367d4e3aefc5b65e0d2d8e4e5bf Mon Sep 17 00:00:00 2001 From: prastoin Date: Mon, 14 Oct 2024 12:01:48 +0200 Subject: [PATCH 1/6] chore: bugs occurs in tests --- lib/src/generateZodClientFromOpenAPI.ts | 28 +- lib/src/template-context.ts | 12 +- ...ody-with-chains-tag-group-strategy.test.ts | 410 +++++++++++++++++- 3 files changed, 431 insertions(+), 19 deletions(-) diff --git a/lib/src/generateZodClientFromOpenAPI.ts b/lib/src/generateZodClientFromOpenAPI.ts index 586f3d70..442cb9a8 100644 --- a/lib/src/generateZodClientFromOpenAPI.ts +++ b/lib/src/generateZodClientFromOpenAPI.ts @@ -17,13 +17,13 @@ type GenerateZodClientFromOpenApiArgs; } & ( - | { - distPath?: never; - /** when true, will only return the result rather than writing it to a file, mostly used for easier testing purpose */ - disableWriteToFile: true; - } - | { distPath: string; disableWriteToFile?: false } -); + | { + distPath?: never; + /** when true, will only return the result rather than writing it to a file, mostly used for easier testing purpose */ + disableWriteToFile: true; + } + | { distPath: string; disableWriteToFile?: false } + ); export const generateZodClientFromOpenAPI = async ({ openApiDoc, @@ -35,14 +35,15 @@ export const generateZodClientFromOpenAPI = async ): Promise< TOptions extends NonNullable - ? undefined extends TOptions["groupStrategy"] - ? string - : TOptions["groupStrategy"] extends "none" | "tag" | "method" - ? string - : Record - : string + ? undefined extends TOptions["groupStrategy"] + ? string + : TOptions["groupStrategy"] extends "none" | "tag" | "method" + ? string + : Record + : string > => { const data = getZodClientTemplateContext(openApiDoc, options); + // console.log(data) const groupStrategy = options?.groupStrategy ?? "none"; if (!templatePath) { @@ -83,6 +84,7 @@ export const generateZodClientFromOpenAPI = async 0) { const commonOutput = maybePretty( commonTemplate({ diff --git a/lib/src/template-context.ts b/lib/src/template-context.ts index 48a61ea7..99b45ae6 100644 --- a/lib/src/template-context.ts +++ b/lib/src/template-context.ts @@ -3,6 +3,7 @@ import { sortBy, sortListFromRefArray, sortObjKeysFromArray } from "pastable/ser import { ts } from "tanu"; import { match } from "ts-pattern"; +import type { CodeMetaData } from "./CodeMeta"; import { getOpenApiDependencyGraph } from "./getOpenApiDependencyGraph"; import type { EndpointDefinitionWithRefs } from "./getZodiosEndpointDefinitionList"; import { getZodiosEndpointDefinitionList } from "./getZodiosEndpointDefinitionList"; @@ -11,7 +12,6 @@ import { getTypescriptFromOpenApi } from "./openApiToTypescript"; import { getZodSchema } from "./openApiToZod"; import { topologicalSort } from "./topologicalSort"; import { asComponentSchema, normalizeString } from "./utils"; -import type { CodeMetaData } from "./CodeMeta"; const file = ts.createSourceFile("", "", ts.ScriptTarget.ESNext, true); const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); @@ -137,16 +137,19 @@ export const getZodClientTemplateContext = ( const dependencies = dependenciesByGroupName.get(groupName)!; const addDependencyIfNeeded = (schemaName: string) => { + console.log(schemaName) if (!schemaName) return; if (schemaName.startsWith("z.")) return; // Sometimes the schema includes a chain that should be removed from the dependency - const [normalizedSchemaName] = schemaName.split("."); - dependencies.add(normalizedSchemaName!); + // const [normalizedSchemaName] = schemaName.split("."); + // console.log({normalizedSchemaName}) + dependencies.add(schemaName!); }; addDependencyIfNeeded(endpoint.response); endpoint.parameters.forEach((param) => addDependencyIfNeeded(param.schema)); endpoint.errors.forEach((param) => addDependencyIfNeeded(param.schema)); + console.log(dependencies) dependencies.forEach((schemaName) => (group.schemas[schemaName] = data.schemas[schemaName]!)); // reduce types/schemas for each group using prev computed deep dependencies @@ -173,6 +176,9 @@ export const getZodClientTemplateContext = ( data.endpoints = sortBy(data.endpoints, "path"); + console.log(data) + console.log(data.endpointsGroups["controller_foo"]) + // Should this be handled above ?? before this parse if (groupStrategy.includes("file")) { const dependenciesCount = new Map(); dependenciesByGroupName.forEach((deps) => { diff --git a/lib/tests/array-body-with-chains-tag-group-strategy.test.ts b/lib/tests/array-body-with-chains-tag-group-strategy.test.ts index aa0d3676..851fd6dc 100644 --- a/lib/tests/array-body-with-chains-tag-group-strategy.test.ts +++ b/lib/tests/array-body-with-chains-tag-group-strategy.test.ts @@ -2,7 +2,7 @@ import type { OpenAPIObject } from "openapi3-ts"; import { expect, test } from "vitest"; import { generateZodClientFromOpenAPI } from "../src"; -test("array-body-with-chains-tag-group-strategy", async () => { +test.only("array-body-with-chains-tag-group-strategy", async () => { const openApiDoc: OpenAPIObject = { openapi: "3.0.0", info: { title: "Test", version: "1.0.1" }, @@ -56,6 +56,114 @@ test("array-body-with-chains-tag-group-strategy", async () => { "Test": "import { makeApi, Zodios, type ZodiosOptions } from "@zodios/core"; import { z } from "zod"; + + + + const putTest_Body.min(1).max(10) = ; + + export const schemas = { + putTest_Body.min(1).max(10), + }; + + const endpoints = makeApi([ + { + method: "put", + path: "/test", + description: \`Test\`, + requestFormat: "json", + parameters: [ + { + name: "body", + type: "Body", + schema: putTest_Body.min(1).max(10) + }, + ], + response: z.void(), + }, + ]); + + export const TestApi = new Zodios(endpoints); + + export function createApiClient(baseUrl: string, options?: ZodiosOptions) { + return new Zodios(baseUrl, endpoints, options); + } + ", + "__index": "export { TestApi } from "./Test"; + ", + } + `); +}); + +test("array-response-body-with-chains-tag-group-strategy directly typed", async () => { + const openApiDoc: OpenAPIObject = { + openapi: "3.0.0", + info: { title: "Test", version: "1.0.1" }, + paths: { + "/test": { + put: { + summary: "Test", + description: "Test", + tags: ["Test"], + requestBody: { + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "object", + properties: { + testItem: { + type: "string", + }, + }, + additionalProperties: false, + }, + }, + }, + }, + }, + parameters: [], + responses: { + "200": { + description: "Success", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "object", + properties: { + foo: { + type: "string", + }, + bar: { + type: "number", + }, + }, + }, + default: ["one", "two"], + }, + }, + }, + }, + }, + }, + }, + }, + components: {}, + tags: [], + }; + + const output = await generateZodClientFromOpenAPI({ + disableWriteToFile: true, + openApiDoc, + options: { groupStrategy: "tag-file" }, + }); + expect(output).toMatchInlineSnapshot(` + { + "Test": "import { makeApi, Zodios, type ZodiosOptions } from "@zodios/core"; + import { z } from "zod"; + const putTest_Body = z.array(z.object({ testItem: z.string() }).partial()); export const schemas = { @@ -72,10 +180,14 @@ test("array-body-with-chains-tag-group-strategy", async () => { { name: "body", type: "Body", - schema: putTest_Body.min(1).max(10), + schema: putTest_Body, }, ], - response: z.void(), + response: z + .array( + z.object({ foo: z.string(), bar: z.number() }).partial().passthrough() + ) + .default(["one", "two"]), }, ]); @@ -90,3 +202,295 @@ test("array-body-with-chains-tag-group-strategy", async () => { } `); }); + +test("array-response-body-with-chains-tag-group-strategy ref's array", async () => { + const openApiDoc: OpenAPIObject = { + openapi: "3.0.0", + info: { title: "Foo bar api", version: "1.0.1" }, + paths: { + "/foo": { + put: { + summary: "Foo", + description: "Foo", + tags: ["controller-foo"], + requestBody: { + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "object", + properties: { + testItem: { + type: "string", + }, + }, + additionalProperties: false, + }, + }, + }, + }, + }, + parameters: [], + responses: { + "200": { + description: "Success", + content: { + "application/json": { + schema: { + "type": "array", + items: { + "$ref": "#/components/schemas/FooBar" + } + } + } + }, + }, + }, + }, + }, + "/bar": { + put: { + summary: "bar", + description: "Bar", + tags: ["controller-bar"], + requestBody: { + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "object", + properties: { + testItem: { + type: "string", + }, + }, + additionalProperties: false, + }, + }, + }, + }, + }, + parameters: [], + responses: { + "200": { + description: "Success", + content: { + "application/json": { + schema: { + "type": "array", + items: { + "$ref": "#/components/schemas/FooBar" + } + } + } + }, + }, + }, + }, + }, + }, + components: { + schemas: { + FooBar: { + type: "object", + properties: { + foo: { + type: "integer", + }, + bar: { + type: "number", + }, + }, + }, + }, + }, + tags: [], + }; + + const output = await generateZodClientFromOpenAPI({ + disableWriteToFile: true, + openApiDoc, + options: { groupStrategy: "tag-file" }, + }); + expect(output).toMatchInlineSnapshot(` + { + "__common": "import { z } from "zod"; + + export const putFoo_Body = z.array( + z.object({ testItem: z.string() }).partial() + ); + ", + "__index": "export { Controller_fooApi } from "./controller_foo"; + export { Controller_barApi } from "./controller_bar"; + ", + "controller_bar": "import { makeApi, Zodios, type ZodiosOptions } from "@zodios/core"; + import { z } from "zod"; + + import { putFoo_Body } from "./common"; + + const endpoints = makeApi([ + { + method: "put", + path: "/bar", + description: \`Bar\`, + requestFormat: "json", + parameters: [ + { + name: "body", + type: "Body", + schema: putFoo_Body, + }, + ], + response: z.array(FooBar), + }, + ]); + + export const Controller_barApi = new Zodios(endpoints); + + export function createApiClient(baseUrl: string, options?: ZodiosOptions) { + return new Zodios(baseUrl, endpoints, options); + } + ", + "controller_foo": "import { makeApi, Zodios, type ZodiosOptions } from "@zodios/core"; + import { z } from "zod"; + + import { putFoo_Body } from "./common"; + + const endpoints = makeApi([ + { + method: "put", + path: "/foo", + description: \`Foo\`, + requestFormat: "json", + parameters: [ + { + name: "body", + type: "Body", + schema: putFoo_Body, + }, + ], + response: z.array(FooBar), + }, + ]); + + export const Controller_fooApi = new Zodios(endpoints); + + export function createApiClient(baseUrl: string, options?: ZodiosOptions) { + return new Zodios(baseUrl, endpoints, options); + } + ", + } + `); +}); + +test("primitive array response body using ref", async () => { + const openApiDoc: OpenAPIObject = { + openapi: "3.0.0", + info: { title: "Foo bar api", version: "1.0.1" }, + paths: { + "/foo": { + put: { + summary: "Foo", + description: "Foo", + tags: ["controller-foo"], + requestBody: { + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "object", + properties: { + testItem: { + type: "string", + }, + }, + additionalProperties: false, + }, + }, + }, + }, + }, + parameters: [], + responses: { + "200": { + description: "Success", + content: { + "application/json": { + schema: { + "type": "array", + items: { + "$ref": "#/components/schemas/FooBar" + } + } + } + }, + }, + }, + }, + }, + }, + components: { + schemas: { + FooBar: { + type: "object", + properties: { + foo: { + type: "integer", + }, + bar: { + type: "number", + }, + }, + }, + }, + }, + tags: [], + }; + + const output = await generateZodClientFromOpenAPI({ + disableWriteToFile: true, + openApiDoc, + }); + expect(output).toMatchInlineSnapshot(` + "import { makeApi, Zodios, type ZodiosOptions } from "@zodios/core"; + import { z } from "zod"; + + const putFoo_Body = z.array(z.object({ testItem: z.string() }).partial()); + const FooBar = z + .object({ foo: z.number().int(), bar: z.number() }) + .partial() + .passthrough(); + + export const schemas = { + putFoo_Body, + FooBar, + }; + + const endpoints = makeApi([ + { + method: "put", + path: "/foo", + description: \`Foo\`, + requestFormat: "json", + parameters: [ + { + name: "body", + type: "Body", + schema: putFoo_Body, + }, + ], + response: z.array(FooBar), + }, + ]); + + export const api = new Zodios(endpoints); + + export function createApiClient(baseUrl: string, options?: ZodiosOptions) { + return new Zodios(baseUrl, endpoints, options); + } + " + `); +}); From a1eaf0af89ec30f58c7e16ef669758a70917dea8 Mon Sep 17 00:00:00 2001 From: prastoin Date: Fri, 30 Aug 2024 17:57:17 +0200 Subject: [PATCH 2/6] fix(lib): extract schema from zod expression Within complex responses definition while tag-grouping --- lib/src/template-context.ts | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/lib/src/template-context.ts b/lib/src/template-context.ts index 99b45ae6..98379297 100644 --- a/lib/src/template-context.ts +++ b/lib/src/template-context.ts @@ -137,13 +137,25 @@ export const getZodClientTemplateContext = ( const dependencies = dependenciesByGroupName.get(groupName)!; const addDependencyIfNeeded = (schemaName: string) => { - console.log(schemaName) if (!schemaName) return; - if (schemaName.startsWith("z.")) return; + // Responses can refer to both z chaining methods and models + // To be able to import them we need to extract them from the raw zod expression. + // E.g: `z.array(z.union([FooBar.and(Bar).and(Foo), z.union([FooBar, Bar, Foo])]))` + if (schemaName.includes("z.")) { + for (const name in result.zodSchemaByName) { + // Matching whole word to avoid false positive e.g: avoid macthing `Foo` with `FooBar` + const regex = new RegExp(String.raw`\b(${name})\b`); + + if (regex.test(schemaName)) { + dependencies.add(name); + } + } + + return + } // Sometimes the schema includes a chain that should be removed from the dependency - // const [normalizedSchemaName] = schemaName.split("."); - // console.log({normalizedSchemaName}) - dependencies.add(schemaName!); + const [normalizedSchemaName] = schemaName.split("."); + dependencies.add(normalizedSchemaName!); }; addDependencyIfNeeded(endpoint.response); From f144b6649de940a6daa0dbbea2c7a15f1e917ce6 Mon Sep 17 00:00:00 2001 From: prastoin Date: Fri, 30 Aug 2024 18:32:06 +0200 Subject: [PATCH 3/6] test(lib): finalizing tests --- ...ody-with-chains-tag-group-strategy.test.ts | 412 +------------- ...-common-schema-tag-group-responses.test.ts | 507 ++++++++++++++++++ 2 files changed, 511 insertions(+), 408 deletions(-) create mode 100644 lib/tests/extract-common-schema-tag-group-responses.test.ts diff --git a/lib/tests/array-body-with-chains-tag-group-strategy.test.ts b/lib/tests/array-body-with-chains-tag-group-strategy.test.ts index 851fd6dc..cb02fb89 100644 --- a/lib/tests/array-body-with-chains-tag-group-strategy.test.ts +++ b/lib/tests/array-body-with-chains-tag-group-strategy.test.ts @@ -2,7 +2,7 @@ import type { OpenAPIObject } from "openapi3-ts"; import { expect, test } from "vitest"; import { generateZodClientFromOpenAPI } from "../src"; -test.only("array-body-with-chains-tag-group-strategy", async () => { +test("array-body-with-chains-tag-group-strategy", async () => { const openApiDoc: OpenAPIObject = { openapi: "3.0.0", info: { title: "Test", version: "1.0.1" }, @@ -56,114 +56,6 @@ test.only("array-body-with-chains-tag-group-strategy", async () => { "Test": "import { makeApi, Zodios, type ZodiosOptions } from "@zodios/core"; import { z } from "zod"; - - - - const putTest_Body.min(1).max(10) = ; - - export const schemas = { - putTest_Body.min(1).max(10), - }; - - const endpoints = makeApi([ - { - method: "put", - path: "/test", - description: \`Test\`, - requestFormat: "json", - parameters: [ - { - name: "body", - type: "Body", - schema: putTest_Body.min(1).max(10) - }, - ], - response: z.void(), - }, - ]); - - export const TestApi = new Zodios(endpoints); - - export function createApiClient(baseUrl: string, options?: ZodiosOptions) { - return new Zodios(baseUrl, endpoints, options); - } - ", - "__index": "export { TestApi } from "./Test"; - ", - } - `); -}); - -test("array-response-body-with-chains-tag-group-strategy directly typed", async () => { - const openApiDoc: OpenAPIObject = { - openapi: "3.0.0", - info: { title: "Test", version: "1.0.1" }, - paths: { - "/test": { - put: { - summary: "Test", - description: "Test", - tags: ["Test"], - requestBody: { - content: { - "application/json": { - schema: { - type: "array", - items: { - type: "object", - properties: { - testItem: { - type: "string", - }, - }, - additionalProperties: false, - }, - }, - }, - }, - }, - parameters: [], - responses: { - "200": { - description: "Success", - content: { - "application/json": { - schema: { - type: "array", - items: { - type: "object", - properties: { - foo: { - type: "string", - }, - bar: { - type: "number", - }, - }, - }, - default: ["one", "two"], - }, - }, - }, - }, - }, - }, - }, - }, - components: {}, - tags: [], - }; - - const output = await generateZodClientFromOpenAPI({ - disableWriteToFile: true, - openApiDoc, - options: { groupStrategy: "tag-file" }, - }); - expect(output).toMatchInlineSnapshot(` - { - "Test": "import { makeApi, Zodios, type ZodiosOptions } from "@zodios/core"; - import { z } from "zod"; - const putTest_Body = z.array(z.object({ testItem: z.string() }).partial()); export const schemas = { @@ -180,14 +72,10 @@ test("array-response-body-with-chains-tag-group-strategy directly typed", async { name: "body", type: "Body", - schema: putTest_Body, + schema: putTest_Body.min(1).max(10), }, ], - response: z - .array( - z.object({ foo: z.string(), bar: z.number() }).partial().passthrough() - ) - .default(["one", "two"]), + response: z.void(), }, ]); @@ -201,296 +89,4 @@ test("array-response-body-with-chains-tag-group-strategy directly typed", async ", } `); -}); - -test("array-response-body-with-chains-tag-group-strategy ref's array", async () => { - const openApiDoc: OpenAPIObject = { - openapi: "3.0.0", - info: { title: "Foo bar api", version: "1.0.1" }, - paths: { - "/foo": { - put: { - summary: "Foo", - description: "Foo", - tags: ["controller-foo"], - requestBody: { - content: { - "application/json": { - schema: { - type: "array", - items: { - type: "object", - properties: { - testItem: { - type: "string", - }, - }, - additionalProperties: false, - }, - }, - }, - }, - }, - parameters: [], - responses: { - "200": { - description: "Success", - content: { - "application/json": { - schema: { - "type": "array", - items: { - "$ref": "#/components/schemas/FooBar" - } - } - } - }, - }, - }, - }, - }, - "/bar": { - put: { - summary: "bar", - description: "Bar", - tags: ["controller-bar"], - requestBody: { - content: { - "application/json": { - schema: { - type: "array", - items: { - type: "object", - properties: { - testItem: { - type: "string", - }, - }, - additionalProperties: false, - }, - }, - }, - }, - }, - parameters: [], - responses: { - "200": { - description: "Success", - content: { - "application/json": { - schema: { - "type": "array", - items: { - "$ref": "#/components/schemas/FooBar" - } - } - } - }, - }, - }, - }, - }, - }, - components: { - schemas: { - FooBar: { - type: "object", - properties: { - foo: { - type: "integer", - }, - bar: { - type: "number", - }, - }, - }, - }, - }, - tags: [], - }; - - const output = await generateZodClientFromOpenAPI({ - disableWriteToFile: true, - openApiDoc, - options: { groupStrategy: "tag-file" }, - }); - expect(output).toMatchInlineSnapshot(` - { - "__common": "import { z } from "zod"; - - export const putFoo_Body = z.array( - z.object({ testItem: z.string() }).partial() - ); - ", - "__index": "export { Controller_fooApi } from "./controller_foo"; - export { Controller_barApi } from "./controller_bar"; - ", - "controller_bar": "import { makeApi, Zodios, type ZodiosOptions } from "@zodios/core"; - import { z } from "zod"; - - import { putFoo_Body } from "./common"; - - const endpoints = makeApi([ - { - method: "put", - path: "/bar", - description: \`Bar\`, - requestFormat: "json", - parameters: [ - { - name: "body", - type: "Body", - schema: putFoo_Body, - }, - ], - response: z.array(FooBar), - }, - ]); - - export const Controller_barApi = new Zodios(endpoints); - - export function createApiClient(baseUrl: string, options?: ZodiosOptions) { - return new Zodios(baseUrl, endpoints, options); - } - ", - "controller_foo": "import { makeApi, Zodios, type ZodiosOptions } from "@zodios/core"; - import { z } from "zod"; - - import { putFoo_Body } from "./common"; - - const endpoints = makeApi([ - { - method: "put", - path: "/foo", - description: \`Foo\`, - requestFormat: "json", - parameters: [ - { - name: "body", - type: "Body", - schema: putFoo_Body, - }, - ], - response: z.array(FooBar), - }, - ]); - - export const Controller_fooApi = new Zodios(endpoints); - - export function createApiClient(baseUrl: string, options?: ZodiosOptions) { - return new Zodios(baseUrl, endpoints, options); - } - ", - } - `); -}); - -test("primitive array response body using ref", async () => { - const openApiDoc: OpenAPIObject = { - openapi: "3.0.0", - info: { title: "Foo bar api", version: "1.0.1" }, - paths: { - "/foo": { - put: { - summary: "Foo", - description: "Foo", - tags: ["controller-foo"], - requestBody: { - content: { - "application/json": { - schema: { - type: "array", - items: { - type: "object", - properties: { - testItem: { - type: "string", - }, - }, - additionalProperties: false, - }, - }, - }, - }, - }, - parameters: [], - responses: { - "200": { - description: "Success", - content: { - "application/json": { - schema: { - "type": "array", - items: { - "$ref": "#/components/schemas/FooBar" - } - } - } - }, - }, - }, - }, - }, - }, - components: { - schemas: { - FooBar: { - type: "object", - properties: { - foo: { - type: "integer", - }, - bar: { - type: "number", - }, - }, - }, - }, - }, - tags: [], - }; - - const output = await generateZodClientFromOpenAPI({ - disableWriteToFile: true, - openApiDoc, - }); - expect(output).toMatchInlineSnapshot(` - "import { makeApi, Zodios, type ZodiosOptions } from "@zodios/core"; - import { z } from "zod"; - - const putFoo_Body = z.array(z.object({ testItem: z.string() }).partial()); - const FooBar = z - .object({ foo: z.number().int(), bar: z.number() }) - .partial() - .passthrough(); - - export const schemas = { - putFoo_Body, - FooBar, - }; - - const endpoints = makeApi([ - { - method: "put", - path: "/foo", - description: \`Foo\`, - requestFormat: "json", - parameters: [ - { - name: "body", - type: "Body", - schema: putFoo_Body, - }, - ], - response: z.array(FooBar), - }, - ]); - - export const api = new Zodios(endpoints); - - export function createApiClient(baseUrl: string, options?: ZodiosOptions) { - return new Zodios(baseUrl, endpoints, options); - } - " - `); -}); +}); \ No newline at end of file diff --git a/lib/tests/extract-common-schema-tag-group-responses.test.ts b/lib/tests/extract-common-schema-tag-group-responses.test.ts new file mode 100644 index 00000000..b6c5f333 --- /dev/null +++ b/lib/tests/extract-common-schema-tag-group-responses.test.ts @@ -0,0 +1,507 @@ +import type { OpenAPIObject, SchemaObject } from "openapi3-ts"; +import { describe, expect, test } from "vitest"; +import { generateZodClientFromOpenAPI } from "../src"; + +describe("Tag file group strategy resolve common schema import from zod expression responses", () => { + const getMultiTagOpenApiDoc = (schema: SchemaObject) => { + const openApiDoc: OpenAPIObject = { + openapi: "3.0.0", + info: { title: "Foo bar api", version: "1.0.1" }, + paths: { + "/foo": { + put: { + summary: "Foo", + description: "Foo", + tags: ["controller-foo"], + responses: { + "200": { + description: "Success", + content: { + "application/json": { + schema + } + }, + }, + }, + }, + }, + "/bar": { + put: { + summary: "bar", + description: "Bar", + tags: ["controller-bar"], + responses: { + "200": { + description: "Success", + content: { + "application/json": { + schema + } + }, + }, + }, + }, + }, + }, + components: { + schemas: { + FooBar: { + type: "object", + properties: { + foo: { + type: "integer", + }, + bar: { + type: "number", + }, + }, + }, + BarFoo: { + type: "object", + properties: { + foo: { + type: "string", + }, + bar: { + type: "boolean", + }, + }, + }, + Bar: { + type: "object", + properties: { + bar: { + type: "string" + } + } + }, + Foo: { + type: "object", + properties: { + foo: { + type: 'boolean' + } + } + } + }, + }, + tags: [], + }; + return openApiDoc + } + + test("Array of $refs response body should import related common schema", async () => { + const openApiDoc = getMultiTagOpenApiDoc({ + "type": "array", + items: { + "$ref": "#/components/schemas/FooBar" + } + } + ) + const output = await generateZodClientFromOpenAPI({ + disableWriteToFile: true, + openApiDoc, + options: { groupStrategy: "tag-file" }, + }); + // This one is ok good perfect this is the bug + expect(output).toMatchInlineSnapshot(` + { + "__common": "import { z } from "zod"; + + export const FooBar = z + .object({ foo: z.number().int(), bar: z.number() }) + .partial() + .passthrough(); + ", + "__index": "export { Controller_fooApi } from "./controller_foo"; + export { Controller_barApi } from "./controller_bar"; + ", + "controller_bar": "import { makeApi, Zodios, type ZodiosOptions } from "@zodios/core"; + import { z } from "zod"; + + import { FooBar } from "./common"; + + const endpoints = makeApi([ + { + method: "put", + path: "/bar", + description: \`Bar\`, + requestFormat: "json", + response: z.array(FooBar), + }, + ]); + + export const Controller_barApi = new Zodios(endpoints); + + export function createApiClient(baseUrl: string, options?: ZodiosOptions) { + return new Zodios(baseUrl, endpoints, options); + } + ", + "controller_foo": "import { makeApi, Zodios, type ZodiosOptions } from "@zodios/core"; + import { z } from "zod"; + + import { FooBar } from "./common"; + + const endpoints = makeApi([ + { + method: "put", + path: "/foo", + description: \`Foo\`, + requestFormat: "json", + response: z.array(FooBar), + }, + ]); + + export const Controller_fooApi = new Zodios(endpoints); + + export function createApiClient(baseUrl: string, options?: ZodiosOptions) { + return new Zodios(baseUrl, endpoints, options); + } + ", + } + `); + }); + + test("Complex nested intersections response body should import related common schema", async () => { + const openApiDoc = getMultiTagOpenApiDoc({ + "type": "array", + items: { + "oneOf": [ + { + "allOf": [ + { + "$ref": "#/components/schemas/FooBar" + }, + { + "$ref": "#/components/schemas/Bar" + }, + { + "$ref": "#/components/schemas/Foo" + } + ] + }, + { + "oneOf": [ + { + "$ref": "#/components/schemas/FooBar" + }, + { + "$ref": "#/components/schemas/Bar" + }, + { + "$ref": "#/components/schemas/Foo" + } + ] + }, + { + "anyOf": [ + { + "$ref": "#/components/schemas/FooBar" + }, + { + "$ref": "#/components/schemas/Foo" + } + ] + }, + ] + } + }) + + const output = await generateZodClientFromOpenAPI({ + disableWriteToFile: true, + openApiDoc, + options: { groupStrategy: "tag-file" }, + }); + // This one is ok good perfect this is the bug + expect(output).toMatchInlineSnapshot(` + { + "__common": "import { z } from "zod"; + + export const FooBar = z + .object({ foo: z.number().int(), bar: z.number() }) + .partial() + .passthrough(); + export const Bar = z.object({ bar: z.string() }).partial().passthrough(); + export const Foo = z.object({ foo: z.boolean() }).partial().passthrough(); + ", + "__index": "export { Controller_fooApi } from "./controller_foo"; + export { Controller_barApi } from "./controller_bar"; + ", + "controller_bar": "import { makeApi, Zodios, type ZodiosOptions } from "@zodios/core"; + import { z } from "zod"; + + import { FooBar } from "./common"; + import { Bar } from "./common"; + import { Foo } from "./common"; + + const endpoints = makeApi([ + { + method: "put", + path: "/bar", + description: \`Bar\`, + requestFormat: "json", + response: z.array( + z.union([ + FooBar.and(Bar).and(Foo), + z.union([FooBar, Bar, Foo]), + z.union([FooBar, Foo]), + ]) + ), + }, + ]); + + export const Controller_barApi = new Zodios(endpoints); + + export function createApiClient(baseUrl: string, options?: ZodiosOptions) { + return new Zodios(baseUrl, endpoints, options); + } + ", + "controller_foo": "import { makeApi, Zodios, type ZodiosOptions } from "@zodios/core"; + import { z } from "zod"; + + import { FooBar } from "./common"; + import { Bar } from "./common"; + import { Foo } from "./common"; + + const endpoints = makeApi([ + { + method: "put", + path: "/foo", + description: \`Foo\`, + requestFormat: "json", + response: z.array( + z.union([ + FooBar.and(Bar).and(Foo), + z.union([FooBar, Bar, Foo]), + z.union([FooBar, Foo]), + ]) + ), + }, + ]); + + export const Controller_fooApi = new Zodios(endpoints); + + export function createApiClient(baseUrl: string, options?: ZodiosOptions) { + return new Zodios(baseUrl, endpoints, options); + } + ", + } + `); + }); + + test("Import only whole word matching common model from zod expression", async () => { + const openApiDoc: OpenAPIObject = { + openapi: "3.0.0", + info: { title: "Foo bar api", version: "1.0.1" }, + paths: { + "/bar": { + put: { + summary: "bar", + description: "Bar", + tags: ["controller-bar"], + responses: { + "200": { + description: "Success", + content: { + "application/json": { + schema: { + "type": "array", + items: { + allOf: [ + { + "$ref": "#/components/schemas/FooBar" + }, + { + "$ref": "#/components/schemas/Bar" + }, + { + "$ref": "#/components/schemas/Foo" + } + ] + } + } + } + }, + }, + }, + }, + get: { + summary: "bar", + description: "Bar", + tags: ["controller-bar"], + responses: { + "200": { + description: "Success", + content: { + "application/json": { + schema: { + "type": "object", + oneOf: [ + { + "$ref": "#/components/schemas/FooBar" + }, + { + "$ref": "#/components/schemas/Bar" + }, + { + "$ref": "#/components/schemas/Foo" + } + ] + } + } + }, + }, + }, + }, + post: { + summary: "bar", + description: "Bar", + tags: ["should-only-import-foobar-and-foo"], + responses: { + "200": { + description: "Success", + content: { + "application/json": { + schema: { + "type": "object", + oneOf: [ + { + "$ref": "#/components/schemas/FooBar" + }, + { + "$ref": "#/components/schemas/Foo" + } + ] + } + } + }, + }, + }, + }, + }, + }, + components: { + schemas: { + FooBar: { + type: "object", + properties: { + foo: { + type: "integer", + }, + bar: { + type: "number", + }, + }, + }, + BarFoo: { + type: "object", + properties: { + foo: { + type: "string", + }, + bar: { + type: "boolean", + }, + }, + }, + Bar: { + type: "object", + properties: { + bar: { + type: "string" + } + } + }, + Foo: { + type: "object", + properties: { + foo: { + type: 'boolean' + } + } + } + }, + }, + tags: [], + }; + + const output = await generateZodClientFromOpenAPI({ + disableWriteToFile: true, + openApiDoc, + options: { groupStrategy: "tag-file" }, + }); + // This one is ok good perfect this is the bug + expect(output).toMatchInlineSnapshot(` + { + "__common": "import { z } from "zod"; + + export const FooBar = z + .object({ foo: z.number().int(), bar: z.number() }) + .partial() + .passthrough(); + export const Foo = z.object({ foo: z.boolean() }).partial().passthrough(); + ", + "__index": "export { Controller_barApi } from "./controller_bar"; + export { Should_only_import_foobar_and_fooApi } from "./should_only_import_foobar_and_foo"; + ", + "controller_bar": "import { makeApi, Zodios, type ZodiosOptions } from "@zodios/core"; + import { z } from "zod"; + + import { FooBar } from "./common"; + import { Foo } from "./common"; + + const Bar = z.object({ bar: z.string() }).partial().passthrough(); + + export const schemas = { + Bar, + }; + + const endpoints = makeApi([ + { + method: "put", + path: "/bar", + description: \`Bar\`, + requestFormat: "json", + response: z.array(FooBar.and(Bar).and(Foo)), + }, + { + method: "get", + path: "/bar", + description: \`Bar\`, + requestFormat: "json", + response: z.union([FooBar, Bar, Foo]), + }, + ]); + + export const Controller_barApi = new Zodios(endpoints); + + export function createApiClient(baseUrl: string, options?: ZodiosOptions) { + return new Zodios(baseUrl, endpoints, options); + } + ", + "should_only_import_foobar_and_foo": "import { makeApi, Zodios, type ZodiosOptions } from "@zodios/core"; + import { z } from "zod"; + + import { FooBar } from "./common"; + import { Foo } from "./common"; + + const endpoints = makeApi([ + { + method: "post", + path: "/bar", + description: \`Bar\`, + requestFormat: "json", + response: z.union([FooBar, Foo]), + }, + ]); + + export const Should_only_import_foobar_and_fooApi = new Zodios(endpoints); + + export function createApiClient(baseUrl: string, options?: ZodiosOptions) { + return new Zodios(baseUrl, endpoints, options); + } + ", + } + `); + }); +}) \ No newline at end of file From 8322a019234cac0cfd30880c30854861294106b3 Mon Sep 17 00:00:00 2001 From: prastoin Date: Sat, 31 Aug 2024 06:39:18 +0200 Subject: [PATCH 4/6] chore(lib): remove logs --- lib/src/generateZodClientFromOpenAPI.ts | 2 -- lib/src/template-context.ts | 4 ---- 2 files changed, 6 deletions(-) diff --git a/lib/src/generateZodClientFromOpenAPI.ts b/lib/src/generateZodClientFromOpenAPI.ts index 442cb9a8..5e07556d 100644 --- a/lib/src/generateZodClientFromOpenAPI.ts +++ b/lib/src/generateZodClientFromOpenAPI.ts @@ -43,7 +43,6 @@ export const generateZodClientFromOpenAPI = async => { const data = getZodClientTemplateContext(openApiDoc, options); - // console.log(data) const groupStrategy = options?.groupStrategy ?? "none"; if (!templatePath) { @@ -84,7 +83,6 @@ export const generateZodClientFromOpenAPI = async 0) { const commonOutput = maybePretty( commonTemplate({ diff --git a/lib/src/template-context.ts b/lib/src/template-context.ts index 98379297..cb46ba73 100644 --- a/lib/src/template-context.ts +++ b/lib/src/template-context.ts @@ -161,7 +161,6 @@ export const getZodClientTemplateContext = ( addDependencyIfNeeded(endpoint.response); endpoint.parameters.forEach((param) => addDependencyIfNeeded(param.schema)); endpoint.errors.forEach((param) => addDependencyIfNeeded(param.schema)); - console.log(dependencies) dependencies.forEach((schemaName) => (group.schemas[schemaName] = data.schemas[schemaName]!)); // reduce types/schemas for each group using prev computed deep dependencies @@ -188,9 +187,6 @@ export const getZodClientTemplateContext = ( data.endpoints = sortBy(data.endpoints, "path"); - console.log(data) - console.log(data.endpointsGroups["controller_foo"]) - // Should this be handled above ?? before this parse if (groupStrategy.includes("file")) { const dependenciesCount = new Map(); dependenciesByGroupName.forEach((deps) => { From 92263fb380d74d0a1a4fc0ae84d6afc26dc180d0 Mon Sep 17 00:00:00 2001 From: prastoin Date: Sat, 31 Aug 2024 07:54:29 +0200 Subject: [PATCH 5/6] test: more coverage --- lib/src/template-context.ts | 5 +- ...> extract-common-schema-tag-group.test.ts} | 336 ++++++++++++++---- 2 files changed, 268 insertions(+), 73 deletions(-) rename lib/tests/{extract-common-schema-tag-group-responses.test.ts => extract-common-schema-tag-group.test.ts} (63%) diff --git a/lib/src/template-context.ts b/lib/src/template-context.ts index cb46ba73..4ba614a5 100644 --- a/lib/src/template-context.ts +++ b/lib/src/template-context.ts @@ -141,16 +141,15 @@ export const getZodClientTemplateContext = ( // Responses can refer to both z chaining methods and models // To be able to import them we need to extract them from the raw zod expression. // E.g: `z.array(z.union([FooBar.and(Bar).and(Foo), z.union([FooBar, Bar, Foo])]))` - if (schemaName.includes("z.")) { + const isZodExpression = schemaName.includes("z.") + if (isZodExpression) { for (const name in result.zodSchemaByName) { // Matching whole word to avoid false positive e.g: avoid macthing `Foo` with `FooBar` const regex = new RegExp(String.raw`\b(${name})\b`); - if (regex.test(schemaName)) { dependencies.add(name); } } - return } // Sometimes the schema includes a chain that should be removed from the dependency diff --git a/lib/tests/extract-common-schema-tag-group-responses.test.ts b/lib/tests/extract-common-schema-tag-group.test.ts similarity index 63% rename from lib/tests/extract-common-schema-tag-group-responses.test.ts rename to lib/tests/extract-common-schema-tag-group.test.ts index b6c5f333..cb957537 100644 --- a/lib/tests/extract-common-schema-tag-group-responses.test.ts +++ b/lib/tests/extract-common-schema-tag-group.test.ts @@ -1,9 +1,24 @@ -import type { OpenAPIObject, SchemaObject } from "openapi3-ts"; +import type { OpenAPIObject, ParameterObject, ReferenceObject, SchemaObject } from "openapi3-ts"; import { describe, expect, test } from "vitest"; import { generateZodClientFromOpenAPI } from "../src"; describe("Tag file group strategy resolve common schema import from zod expression responses", () => { - const getMultiTagOpenApiDoc = (schema: SchemaObject) => { + type GetMultiTagOpenApiDocArgs = { + responseSchema?: SchemaObject | ReferenceObject + parameters?: Array + } + const getMultiTagOpenApiDoc = ({ parameters, responseSchema: schema }: GetMultiTagOpenApiDocArgs) => { + const responses = schema != undefined ? { + "200": { + description: "Success", + content: { + "application/json": { + schema + } + }, + }, + } : undefined; + const openApiDoc: OpenAPIObject = { openapi: "3.0.0", info: { title: "Foo bar api", version: "1.0.1" }, @@ -13,16 +28,8 @@ describe("Tag file group strategy resolve common schema import from zod expressi summary: "Foo", description: "Foo", tags: ["controller-foo"], - responses: { - "200": { - description: "Success", - content: { - "application/json": { - schema - } - }, - }, - }, + responses, + parameters }, }, "/bar": { @@ -30,16 +37,8 @@ describe("Tag file group strategy resolve common schema import from zod expressi summary: "bar", description: "Bar", tags: ["controller-bar"], - responses: { - "200": { - description: "Success", - content: { - "application/json": { - schema - } - }, - }, - }, + responses, + parameters }, }, }, @@ -90,20 +89,97 @@ describe("Tag file group strategy resolve common schema import from zod expressi return openApiDoc } + test("SchemaObject referring to ReferencedObject response body should import related common schema", async () => { + const openApiDoc = getMultiTagOpenApiDoc( + { + responseSchema: { + type: "object", + properties: { + fooBar: { + "$ref": "#/components/schemas/FooBar" + }, + } + } + } + ) + + const output = await generateZodClientFromOpenAPI({ + disableWriteToFile: true, + openApiDoc, + options: { groupStrategy: "tag-file" }, + }); + expect(output).toMatchInlineSnapshot(` + { + "__common": "import { z } from "zod"; + + export const FooBar = z + .object({ foo: z.number().int(), bar: z.number() }) + .partial() + .passthrough(); + ", + "__index": "export { Controller_fooApi } from "./controller_foo"; + export { Controller_barApi } from "./controller_bar"; + ", + "controller_bar": "import { makeApi, Zodios, type ZodiosOptions } from "@zodios/core"; + import { z } from "zod"; + + import { FooBar } from "./common"; + + const endpoints = makeApi([ + { + method: "put", + path: "/bar", + description: \`Bar\`, + requestFormat: "json", + response: z.object({ fooBar: FooBar }).partial().passthrough(), + }, + ]); + + export const Controller_barApi = new Zodios(endpoints); + + export function createApiClient(baseUrl: string, options?: ZodiosOptions) { + return new Zodios(baseUrl, endpoints, options); + } + ", + "controller_foo": "import { makeApi, Zodios, type ZodiosOptions } from "@zodios/core"; + import { z } from "zod"; + + import { FooBar } from "./common"; + + const endpoints = makeApi([ + { + method: "put", + path: "/foo", + description: \`Foo\`, + requestFormat: "json", + response: z.object({ fooBar: FooBar }).partial().passthrough(), + }, + ]); + + export const Controller_fooApi = new Zodios(endpoints); + + export function createApiClient(baseUrl: string, options?: ZodiosOptions) { + return new Zodios(baseUrl, endpoints, options); + } + ", + } + `); + }); + test("Array of $refs response body should import related common schema", async () => { const openApiDoc = getMultiTagOpenApiDoc({ - "type": "array", - items: { - "$ref": "#/components/schemas/FooBar" + responseSchema: { + "type": "array", + items: { + "$ref": "#/components/schemas/FooBar" + } } - } - ) + }) const output = await generateZodClientFromOpenAPI({ disableWriteToFile: true, openApiDoc, options: { groupStrategy: "tag-file" }, }); - // This one is ok good perfect this is the bug expect(output).toMatchInlineSnapshot(` { "__common": "import { z } from "zod"; @@ -164,46 +240,48 @@ describe("Tag file group strategy resolve common schema import from zod expressi test("Complex nested intersections response body should import related common schema", async () => { const openApiDoc = getMultiTagOpenApiDoc({ - "type": "array", - items: { - "oneOf": [ - { - "allOf": [ - { - "$ref": "#/components/schemas/FooBar" - }, - { - "$ref": "#/components/schemas/Bar" - }, - { - "$ref": "#/components/schemas/Foo" - } - ] - }, - { - "oneOf": [ - { - "$ref": "#/components/schemas/FooBar" - }, - { - "$ref": "#/components/schemas/Bar" - }, - { - "$ref": "#/components/schemas/Foo" - } - ] - }, - { - "anyOf": [ - { - "$ref": "#/components/schemas/FooBar" - }, - { - "$ref": "#/components/schemas/Foo" - } - ] - }, - ] + responseSchema: { + "type": "array", + items: { + "oneOf": [ + { + "allOf": [ + { + "$ref": "#/components/schemas/FooBar" + }, + { + "$ref": "#/components/schemas/Bar" + }, + { + "$ref": "#/components/schemas/Foo" + } + ] + }, + { + "oneOf": [ + { + "$ref": "#/components/schemas/FooBar" + }, + { + "$ref": "#/components/schemas/Bar" + }, + { + "$ref": "#/components/schemas/Foo" + } + ] + }, + { + "anyOf": [ + { + "$ref": "#/components/schemas/FooBar" + }, + { + "$ref": "#/components/schemas/Foo" + } + ] + }, + ] + } } }) @@ -212,7 +290,6 @@ describe("Tag file group strategy resolve common schema import from zod expressi openApiDoc, options: { groupStrategy: "tag-file" }, }); - // This one is ok good perfect this is the bug expect(output).toMatchInlineSnapshot(` { "__common": "import { z } from "zod"; @@ -289,6 +366,126 @@ describe("Tag file group strategy resolve common schema import from zod expressi `); }); + test("SchemaObject referring to ReferencedObject through parameters should import related common schema", async () => { + const openApiDoc = getMultiTagOpenApiDoc({ + parameters: [ + { + in: "query", + name: "array-ref-object", + schema: { + type: "array", + items: { $ref: "#/components/schemas/Bar" }, + default: [{ id: 1, name: "foo" }], + }, + }, + { + in: "query", + name: "array-ref-enum", + schema: { + type: "array", + items: { $ref: "#/components/schemas/FooBar" }, + default: ["one", "two"], + }, + }, + ] + }) + + const output = await generateZodClientFromOpenAPI({ + disableWriteToFile: true, + openApiDoc, + options: { groupStrategy: "tag-file" }, + }); + expect(output).toMatchInlineSnapshot(` + { + "__common": "import { z } from "zod"; + + export const Bar = z.object({ bar: z.string() }).partial().passthrough(); + export const FooBar = z + .object({ foo: z.number().int(), bar: z.number() }) + .partial() + .passthrough(); + ", + "__index": "export { Controller_fooApi } from "./controller_foo"; + export { Controller_barApi } from "./controller_bar"; + ", + "controller_bar": "import { makeApi, Zodios, type ZodiosOptions } from "@zodios/core"; + import { z } from "zod"; + + import { Bar } from "./common"; + import { FooBar } from "./common"; + + const endpoints = makeApi([ + { + method: "put", + path: "/bar", + description: \`Bar\`, + requestFormat: "json", + parameters: [ + { + name: "array-ref-object", + type: "Query", + schema: z + .array(Bar) + .optional() + .default([{ id: 1, name: "foo" }]), + }, + { + name: "array-ref-enum", + type: "Query", + schema: z.array(FooBar).optional().default(["one", "two"]), + }, + ], + response: z.void(), + }, + ]); + + export const Controller_barApi = new Zodios(endpoints); + + export function createApiClient(baseUrl: string, options?: ZodiosOptions) { + return new Zodios(baseUrl, endpoints, options); + } + ", + "controller_foo": "import { makeApi, Zodios, type ZodiosOptions } from "@zodios/core"; + import { z } from "zod"; + + import { Bar } from "./common"; + import { FooBar } from "./common"; + + const endpoints = makeApi([ + { + method: "put", + path: "/foo", + description: \`Foo\`, + requestFormat: "json", + parameters: [ + { + name: "array-ref-object", + type: "Query", + schema: z + .array(Bar) + .optional() + .default([{ id: 1, name: "foo" }]), + }, + { + name: "array-ref-enum", + type: "Query", + schema: z.array(FooBar).optional().default(["one", "two"]), + }, + ], + response: z.void(), + }, + ]); + + export const Controller_fooApi = new Zodios(endpoints); + + export function createApiClient(baseUrl: string, options?: ZodiosOptions) { + return new Zodios(baseUrl, endpoints, options); + } + ", + } + `); + }); + test("Import only whole word matching common model from zod expression", async () => { const openApiDoc: OpenAPIObject = { openapi: "3.0.0", @@ -430,7 +627,6 @@ describe("Tag file group strategy resolve common schema import from zod expressi openApiDoc, options: { groupStrategy: "tag-file" }, }); - // This one is ok good perfect this is the bug expect(output).toMatchInlineSnapshot(` { "__common": "import { z } from "zod"; From 2fe9ff0208b9405cc09f19e32b9f2525d4868b8c Mon Sep 17 00:00:00 2001 From: prastoin Date: Mon, 14 Oct 2024 11:47:59 +0200 Subject: [PATCH 6/6] chore: revert useless lint and final new line --- lib/src/generateZodClientFromOpenAPI.ts | 26 +-- lib/src/template-context.ts | 2 +- ...ody-with-chains-tag-group-strategy.test.ts | 2 +- .../extract-common-schema-tag-group.test.ts | 197 +++++++++--------- 4 files changed, 114 insertions(+), 113 deletions(-) diff --git a/lib/src/generateZodClientFromOpenAPI.ts b/lib/src/generateZodClientFromOpenAPI.ts index 5e07556d..586f3d70 100644 --- a/lib/src/generateZodClientFromOpenAPI.ts +++ b/lib/src/generateZodClientFromOpenAPI.ts @@ -17,13 +17,13 @@ type GenerateZodClientFromOpenApiArgs; } & ( - | { - distPath?: never; - /** when true, will only return the result rather than writing it to a file, mostly used for easier testing purpose */ - disableWriteToFile: true; - } - | { distPath: string; disableWriteToFile?: false } - ); + | { + distPath?: never; + /** when true, will only return the result rather than writing it to a file, mostly used for easier testing purpose */ + disableWriteToFile: true; + } + | { distPath: string; disableWriteToFile?: false } +); export const generateZodClientFromOpenAPI = async ({ openApiDoc, @@ -35,12 +35,12 @@ export const generateZodClientFromOpenAPI = async ): Promise< TOptions extends NonNullable - ? undefined extends TOptions["groupStrategy"] - ? string - : TOptions["groupStrategy"] extends "none" | "tag" | "method" - ? string - : Record - : string + ? undefined extends TOptions["groupStrategy"] + ? string + : TOptions["groupStrategy"] extends "none" | "tag" | "method" + ? string + : Record + : string > => { const data = getZodClientTemplateContext(openApiDoc, options); const groupStrategy = options?.groupStrategy ?? "none"; diff --git a/lib/src/template-context.ts b/lib/src/template-context.ts index 4ba614a5..e67c164b 100644 --- a/lib/src/template-context.ts +++ b/lib/src/template-context.ts @@ -3,7 +3,6 @@ import { sortBy, sortListFromRefArray, sortObjKeysFromArray } from "pastable/ser import { ts } from "tanu"; import { match } from "ts-pattern"; -import type { CodeMetaData } from "./CodeMeta"; import { getOpenApiDependencyGraph } from "./getOpenApiDependencyGraph"; import type { EndpointDefinitionWithRefs } from "./getZodiosEndpointDefinitionList"; import { getZodiosEndpointDefinitionList } from "./getZodiosEndpointDefinitionList"; @@ -12,6 +11,7 @@ import { getTypescriptFromOpenApi } from "./openApiToTypescript"; import { getZodSchema } from "./openApiToZod"; import { topologicalSort } from "./topologicalSort"; import { asComponentSchema, normalizeString } from "./utils"; +import type { CodeMetaData } from "./CodeMeta"; const file = ts.createSourceFile("", "", ts.ScriptTarget.ESNext, true); const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); diff --git a/lib/tests/array-body-with-chains-tag-group-strategy.test.ts b/lib/tests/array-body-with-chains-tag-group-strategy.test.ts index cb02fb89..aa0d3676 100644 --- a/lib/tests/array-body-with-chains-tag-group-strategy.test.ts +++ b/lib/tests/array-body-with-chains-tag-group-strategy.test.ts @@ -89,4 +89,4 @@ test("array-body-with-chains-tag-group-strategy", async () => { ", } `); -}); \ No newline at end of file +}); diff --git a/lib/tests/extract-common-schema-tag-group.test.ts b/lib/tests/extract-common-schema-tag-group.test.ts index cb957537..4e4ef9fb 100644 --- a/lib/tests/extract-common-schema-tag-group.test.ts +++ b/lib/tests/extract-common-schema-tag-group.test.ts @@ -4,20 +4,23 @@ import { generateZodClientFromOpenAPI } from "../src"; describe("Tag file group strategy resolve common schema import from zod expression responses", () => { type GetMultiTagOpenApiDocArgs = { - responseSchema?: SchemaObject | ReferenceObject - parameters?: Array - } + responseSchema?: SchemaObject | ReferenceObject; + parameters?: Array; + }; const getMultiTagOpenApiDoc = ({ parameters, responseSchema: schema }: GetMultiTagOpenApiDocArgs) => { - const responses = schema != undefined ? { - "200": { - description: "Success", - content: { - "application/json": { - schema - } - }, - }, - } : undefined; + const responses = + schema != undefined + ? { + "200": { + description: "Success", + content: { + "application/json": { + schema, + }, + }, + }, + } + : undefined; const openApiDoc: OpenAPIObject = { openapi: "3.0.0", @@ -29,7 +32,7 @@ describe("Tag file group strategy resolve common schema import from zod expressi description: "Foo", tags: ["controller-foo"], responses, - parameters + parameters, }, }, "/bar": { @@ -38,7 +41,7 @@ describe("Tag file group strategy resolve common schema import from zod expressi description: "Bar", tags: ["controller-bar"], responses, - parameters + parameters, }, }, }, @@ -70,38 +73,36 @@ describe("Tag file group strategy resolve common schema import from zod expressi type: "object", properties: { bar: { - type: "string" - } - } + type: "string", + }, + }, }, Foo: { type: "object", properties: { foo: { - type: 'boolean' - } - } - } + type: "boolean", + }, + }, + }, }, }, tags: [], }; - return openApiDoc - } + return openApiDoc; + }; test("SchemaObject referring to ReferencedObject response body should import related common schema", async () => { - const openApiDoc = getMultiTagOpenApiDoc( - { - responseSchema: { - type: "object", - properties: { - fooBar: { - "$ref": "#/components/schemas/FooBar" - }, - } - } - } - ) + const openApiDoc = getMultiTagOpenApiDoc({ + responseSchema: { + type: "object", + properties: { + fooBar: { + $ref: "#/components/schemas/FooBar", + }, + }, + }, + }); const output = await generateZodClientFromOpenAPI({ disableWriteToFile: true, @@ -169,12 +170,12 @@ describe("Tag file group strategy resolve common schema import from zod expressi test("Array of $refs response body should import related common schema", async () => { const openApiDoc = getMultiTagOpenApiDoc({ responseSchema: { - "type": "array", + type: "array", items: { - "$ref": "#/components/schemas/FooBar" - } - } - }) + $ref: "#/components/schemas/FooBar", + }, + }, + }); const output = await generateZodClientFromOpenAPI({ disableWriteToFile: true, openApiDoc, @@ -241,49 +242,49 @@ describe("Tag file group strategy resolve common schema import from zod expressi test("Complex nested intersections response body should import related common schema", async () => { const openApiDoc = getMultiTagOpenApiDoc({ responseSchema: { - "type": "array", + type: "array", items: { - "oneOf": [ + oneOf: [ { - "allOf": [ + allOf: [ { - "$ref": "#/components/schemas/FooBar" + $ref: "#/components/schemas/FooBar", }, { - "$ref": "#/components/schemas/Bar" + $ref: "#/components/schemas/Bar", }, { - "$ref": "#/components/schemas/Foo" - } - ] + $ref: "#/components/schemas/Foo", + }, + ], }, { - "oneOf": [ + oneOf: [ { - "$ref": "#/components/schemas/FooBar" + $ref: "#/components/schemas/FooBar", }, { - "$ref": "#/components/schemas/Bar" + $ref: "#/components/schemas/Bar", }, { - "$ref": "#/components/schemas/Foo" - } - ] + $ref: "#/components/schemas/Foo", + }, + ], }, { - "anyOf": [ + anyOf: [ { - "$ref": "#/components/schemas/FooBar" + $ref: "#/components/schemas/FooBar", }, { - "$ref": "#/components/schemas/Foo" - } - ] + $ref: "#/components/schemas/Foo", + }, + ], }, - ] - } - } - }) + ], + }, + }, + }); const output = await generateZodClientFromOpenAPI({ disableWriteToFile: true, @@ -387,8 +388,8 @@ describe("Tag file group strategy resolve common schema import from zod expressi default: ["one", "two"], }, }, - ] - }) + ], + }); const output = await generateZodClientFromOpenAPI({ disableWriteToFile: true, @@ -502,22 +503,22 @@ describe("Tag file group strategy resolve common schema import from zod expressi content: { "application/json": { schema: { - "type": "array", + type: "array", items: { allOf: [ { - "$ref": "#/components/schemas/FooBar" + $ref: "#/components/schemas/FooBar", }, { - "$ref": "#/components/schemas/Bar" + $ref: "#/components/schemas/Bar", }, { - "$ref": "#/components/schemas/Foo" - } - ] - } - } - } + $ref: "#/components/schemas/Foo", + }, + ], + }, + }, + }, }, }, }, @@ -532,20 +533,20 @@ describe("Tag file group strategy resolve common schema import from zod expressi content: { "application/json": { schema: { - "type": "object", + type: "object", oneOf: [ { - "$ref": "#/components/schemas/FooBar" + $ref: "#/components/schemas/FooBar", }, { - "$ref": "#/components/schemas/Bar" + $ref: "#/components/schemas/Bar", }, { - "$ref": "#/components/schemas/Foo" - } - ] - } - } + $ref: "#/components/schemas/Foo", + }, + ], + }, + }, }, }, }, @@ -560,17 +561,17 @@ describe("Tag file group strategy resolve common schema import from zod expressi content: { "application/json": { schema: { - "type": "object", + type: "object", oneOf: [ { - "$ref": "#/components/schemas/FooBar" + $ref: "#/components/schemas/FooBar", }, { - "$ref": "#/components/schemas/Foo" - } - ] - } - } + $ref: "#/components/schemas/Foo", + }, + ], + }, + }, }, }, }, @@ -605,18 +606,18 @@ describe("Tag file group strategy resolve common schema import from zod expressi type: "object", properties: { bar: { - type: "string" - } - } + type: "string", + }, + }, }, Foo: { type: "object", properties: { foo: { - type: 'boolean' - } - } - } + type: "boolean", + }, + }, + }, }, }, tags: [], @@ -700,4 +701,4 @@ describe("Tag file group strategy resolve common schema import from zod expressi } `); }); -}) \ No newline at end of file +});