Skip to content

Feature implementation from commits 89b41b5..494583f #4

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 15 commits into
base: feature-base-branch-2
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions lib/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,35 @@
# openapi-zod-client

## 1.17.0

### Minor Changes

- [#283](https://github.com/astahmer/openapi-zod-client/pull/283) [`3ec4915`](https://github.com/astahmer/openapi-zod-client/commit/3ec491572e56fc40e3b49cefb58cb6f08600190f) Thanks [@dgadelha](https://github.com/dgadelha)! - Add `schemaRefiner` option to allow refining the OpenAPI schema before its converted to a Zod schema

## 1.16.4

### Patch Changes

- [#279](https://github.com/astahmer/openapi-zod-client/pull/279) [`f3ee25e`](https://github.com/astahmer/openapi-zod-client/commit/f3ee25efc191d0be97231498924fe50fd977fb88) Thanks [@dgadelha](https://github.com/dgadelha)! - Fix multiline descriptions when `describe` is enabled

## 1.16.3

### Patch Changes

- [#276](https://github.com/astahmer/openapi-zod-client/pull/276) [`aa4c7a3`](https://github.com/astahmer/openapi-zod-client/commit/aa4c7a3668c6d96492bcd319ccd940f0b735b029) Thanks [@tankers746](https://github.com/tankers746)! - Fixed bug which was excluding falsy default values

## 1.16.2

### Patch Changes

- [#271](https://github.com/astahmer/openapi-zod-client/pull/271) [`197316b`](https://github.com/astahmer/openapi-zod-client/commit/197316b50b0b84cea977984ae82441f2ce108ea0) Thanks [@codingmatty](https://github.com/codingmatty)! - Fix invalid output when using array types as the endpoint body with minItems or maxItems and using the tag-file group-strategy.

## 1.16.1

### Patch Changes

- [#270](https://github.com/astahmer/openapi-zod-client/pull/270) [`04dd1b5`](https://github.com/astahmer/openapi-zod-client/commit/04dd1b549118c8b8e5a3b86f6dbed741f44770c8) Thanks [@codingmatty](https://github.com/codingmatty)! - Fix bug with `exportAllNamedSchemas` option where schemas will reuse last schema name with matching schema rather than it's own name that has already been used before.

## 1.16.0

### Minor Changes
Expand Down
2 changes: 1 addition & 1 deletion lib/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "openapi-zod-client",
"version": "1.16.0",
"version": "1.17.0",
"repository": {
"type": "git",
"url": "https://github.com/astahmer/openapi-zod-client.git"
Expand Down
1 change: 1 addition & 0 deletions lib/src/CodeMeta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type ConversionTypeContext = {
resolver: DocumentResolver;
zodSchemaByName: Record<string, string>;
schemaByName: Record<string, string>;
schemasByName?: Record<string, string[]>;
};

export type CodeMetaData = {
Expand Down
21 changes: 17 additions & 4 deletions lib/src/getZodiosEndpointDefinitionList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ export const getZodiosEndpointDefinitionList = (doc: OpenAPIObject, options?: Te
.otherwise((fn) => fn);

const ctx: ConversionTypeContext = { resolver, zodSchemaByName: {}, schemaByName: {} };
if (options?.exportAllNamedSchemas) {
ctx.schemasByName = {};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🐛 Correctness Issue

Property name mismatch with type definition.

The code initializes ctx.schemasByName but the type definition uses schemaByName (without 's'), which will cause runtime errors when accessing this property.

Current Code (Diff):

-        ctx.schemasByName = {};
+        ctx.schemaByName = {};
📝 Committable suggestion

‼️ IMPORTANT
Trust, but verify! 🕵️ Please review this suggestion with the care of a code archaeologist - check that it perfectly replaces the highlighted code, preserves all lines, maintains proper indentation, and won't break anything in production. Your future self will thank you! 🚀

Suggested change
ctx.schemasByName = {};
ctx.schemaByName = {};

🔄 Dependencies Affected

lib/src/template-context.ts

Function: any function using getZodiosEndpointDefinitionList result

Issue: Code expecting schemaByName property but receiving schemasByName

Suggestion: Update any code using ctx.schemasByName to use ctx.schemaByName instead


}

const complexityThreshold = options?.complexityThreshold ?? 4;
const getZodVarName = (input: CodeMeta, fallbackName?: string) => {
const result = input.toString();
Expand Down Expand Up @@ -95,8 +99,8 @@ export const getZodiosEndpointDefinitionList = (doc: OpenAPIObject, options?: Te
let isVarNameAlreadyUsed = false;
while ((isVarNameAlreadyUsed = Boolean(ctx.zodSchemaByName[formatedName]))) {
if (isVarNameAlreadyUsed) {
if (options?.exportAllNamedSchemas && ctx.schemaByName[result]) {
return ctx.schemaByName[result]!;
if (options?.exportAllNamedSchemas && ctx.schemasByName?.[result]?.includes(formatedName)) {
return formatedName;
} else if (ctx.zodSchemaByName[formatedName] === safeName) {
return formatedName;
} else {
Expand All @@ -108,6 +112,11 @@ export const getZodiosEndpointDefinitionList = (doc: OpenAPIObject, options?: Te

ctx.zodSchemaByName[formatedName] = result;
ctx.schemaByName[result] = formatedName;

if (options?.exportAllNamedSchemas && ctx.schemasByName) {
ctx.schemasByName[result] = (ctx.schemasByName[result] ?? []).concat(formatedName);
}

return formatedName;
}

Expand Down Expand Up @@ -240,7 +249,7 @@ export const getZodiosEndpointDefinitionList = (doc: OpenAPIObject, options?: Te
}

if (options?.withDescription && paramSchema) {
(paramSchema as SchemaObject).description = (paramItem.description ?? "")?.replace("\n", "");
(paramSchema as SchemaObject).description = (paramItem.description ?? "").trim();
}

// resolve ref if needed, and fallback to default (unknown) value if needed
Expand Down Expand Up @@ -309,7 +318,11 @@ export const getZodiosEndpointDefinitionList = (doc: OpenAPIObject, options?: Te
}

if (endpointDefinition.responses !== undefined) {
endpointDefinition.responses.push({ statusCode, schema: schemaString ?? voidSchema, description: responseItem.description });
endpointDefinition.responses.push({
statusCode,
schema: schemaString ?? voidSchema,
description: responseItem.description,
});
}

if (schemaString) {
Expand Down
14 changes: 10 additions & 4 deletions lib/src/openApiToZod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ type ConversionArgs = {
* @see https://github.com/colinhacks/zod
*/
// eslint-disable-next-line sonarjs/cognitive-complexity
export function getZodSchema({ schema, ctx, meta: inheritedMeta, options }: ConversionArgs): CodeMeta {
if (!schema) {
export function getZodSchema({ schema: $schema, ctx, meta: inheritedMeta, options }: ConversionArgs): CodeMeta {
if (!$schema) {
throw new Error("Schema is required");
}

const schema = options?.schemaRefiner?.($schema, inheritedMeta) ?? $schema;
const code = new CodeMeta(schema, ctx, inheritedMeta);
const meta = {
parent: code.inherit(inheritedMeta?.parent),
Expand Down Expand Up @@ -302,7 +304,11 @@ export const getZodChain = ({ schema, meta, options }: ZodChainArgs) => {
.otherwise(() => void 0);

if (typeof schema.description === "string" && schema.description !== "" && options?.withDescription) {
chains.push(`describe("${schema.description}")`);
if (["\n", "\r", "\r\n"].some((c) => String.prototype.includes.call(schema.description, c))) {
chains.push(`describe(\`${schema.description}\`)`);
} else {
chains.push(`describe("${schema.description}")`);
}
}

const output = chains
Expand Down Expand Up @@ -341,7 +347,7 @@ const unwrapQuotesIfNeeded = (value: string | number) => {
};

const getZodChainableDefault = (schema: SchemaObject) => {
if (schema.default) {
if (schema.default !== undefined) {
const value = match(schema.type)
.with("number", "integer", () => unwrapQuotesIfNeeded(schema.default))
.otherwise(() => JSON.stringify(schema.default));
Expand Down
14 changes: 11 additions & 3 deletions lib/src/template-context.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { OpenAPIObject, OperationObject, PathItemObject, SchemaObject } from "openapi3-ts";
import type { OpenAPIObject, OperationObject, PathItemObject, ReferenceObject, SchemaObject } from "openapi3-ts";
import { sortBy, sortListFromRefArray, sortObjKeysFromArray } from "pastable/server";
import { ts } from "tanu";
import { match } from "ts-pattern";
Expand All @@ -11,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 });
Expand Down Expand Up @@ -138,7 +139,9 @@ export const getZodClientTemplateContext = (
const addDependencyIfNeeded = (schemaName: string) => {
if (!schemaName) return;
if (schemaName.startsWith("z.")) return;
dependencies.add(schemaName);
// Sometimes the schema includes a chain that should be removed from the dependency
const [normalizedSchemaName] = schemaName.split(".");
dependencies.add(normalizedSchemaName!);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🐛 Correctness Issue

Non-null Assertion on Potentially Undefined Value.

The non-null assertion operator on normalizedSchemaName could cause runtime errors if the split pattern produces unexpected results.

Current Code (Diff):

-                 dependencies.add(normalizedSchemaName!);
+                 dependencies.add(normalizedSchemaName || schemaName);
📝 Committable suggestion

‼️ IMPORTANT
Trust, but verify! 🕵️ Please review this suggestion with the care of a code archaeologist - check that it perfectly replaces the highlighted code, preserves all lines, maintains proper indentation, and won't break anything in production. Your future self will thank you! 🚀

Suggested change
dependencies.add(normalizedSchemaName!);
dependencies.add(normalizedSchemaName || schemaName);

};

addDependencyIfNeeded(endpoint.response);
Expand Down Expand Up @@ -394,11 +397,16 @@ export type TemplateContextOptions = {
* When true, returns a "responses" array with all responses (both success and errors)
*/
withAllResponses?: boolean;

/**
* When true, prevents using the exact same name for the same type
* For example, if 2 schemas have the same type, but different names, export each as separate schemas
* If 2 schemas have the same name but different types, export subsequent names with numbers appended
*/
exportAllNamedSchemas?: boolean;

/**
* A function that runs in the schema conversion process to refine the schema before it's converted to a Zod schema.
*/
schemaRefiner?: <T extends SchemaObject | ReferenceObject>(schema: T, parentMeta?: CodeMetaData) => T;
};
92 changes: 92 additions & 0 deletions lib/tests/array-body-with-chains-tag-group-strategy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import type { OpenAPIObject } from "openapi3-ts";
import { expect, test } from "vitest";
import { generateZodClientFromOpenAPI } from "../src";

test("array-body-with-chains-tag-group-strategy", 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,
},
minItems: 1,
maxItems: 10,
},
},
},
},
parameters: [],
responses: {
"200": {
description: "Success",
content: { "application/json": {} },
},
},
},
},
},
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 = {
putTest_Body,
};

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";
",
}
`);
});
32 changes: 32 additions & 0 deletions lib/tests/description-in-zod.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,23 @@ test("description-in-zod", async () => {
},
description: "bar description",
},
{
in: "query",
name: "baz",
schema: {
type: "number",
enum: [1.3, 34.1, -57.89],
},
description: "baz\nmultiline\ndescription",
},
{
in: "query",
name: "qux",
schema: {
type: "string",
},
description: " ", // spaces only description
},
],
responses: {
"200": {
Expand Down Expand Up @@ -73,6 +90,21 @@ test("description-in-zod", async () => {
.describe("bar description")
.optional(),
},
{
name: "baz",
type: "Query",
schema: z
.union([z.literal(1.3), z.literal(34.1), z.literal(-57.89)])
.describe(
\`baz\nmultiline\ndescription\`
)
.optional(),
},
{
name: "qux",
type: "Query",
schema: z.string().optional(),
},
],
response: z.void(),
},
Expand Down
18 changes: 18 additions & 0 deletions lib/tests/export-all-named-schemas.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ test("export-all-named-schemas", async () => {
},
},
parameters: [
{
name: "sameSchemaDifferentName",
in: "query",
schema: { type: "string", enum: ["xxx", "yyy", "zzz"] },
},
{
name: "sameSchemaSameName",
in: "query",
Expand Down Expand Up @@ -128,6 +133,11 @@ test("export-all-named-schemas", async () => {
"errors": [],
"method": "delete",
"parameters": [
{
"name": "sameSchemaDifferentName",
"schema": "sameSchemaDifferentName",
"type": "Query",
},
{
"name": "sameSchemaSameName",
"schema": "sameSchemaSameName",
Expand Down Expand Up @@ -160,6 +170,7 @@ test("export-all-named-schemas", async () => {
"withAlias": false,
},
"schemas": {
"sameSchemaDifferentName": "z.enum(["xxx", "yyy", "zzz"]).optional()",
"sameSchemaSameName": "z.enum(["xxx", "yyy", "zzz"]).optional()",
"schemaNameAlreadyUsed": "z.enum(["aaa", "bbb", "ccc"]).optional()",
"schemaNameAlreadyUsed__2": "z.enum(["ggg", "hhh", "iii"]).optional()",
Expand All @@ -180,11 +191,13 @@ test("export-all-named-schemas", async () => {

const sameSchemaSameName = z.enum(["xxx", "yyy", "zzz"]).optional();
const schemaNameAlreadyUsed = z.enum(["aaa", "bbb", "ccc"]).optional();
const sameSchemaDifferentName = z.enum(["xxx", "yyy", "zzz"]).optional();
const schemaNameAlreadyUsed__2 = z.enum(["ggg", "hhh", "iii"]).optional();

export const schemas = {
sameSchemaSameName,
schemaNameAlreadyUsed,
sameSchemaDifferentName,
schemaNameAlreadyUsed__2,
};

Expand Down Expand Up @@ -220,6 +233,11 @@ test("export-all-named-schemas", async () => {
path: "/export-all-named-schemas",
requestFormat: "json",
parameters: [
{
name: "sameSchemaDifferentName",
type: "Query",
schema: sameSchemaDifferentName,
},
{
name: "sameSchemaSameName",
type: "Query",
Expand Down
2 changes: 1 addition & 1 deletion lib/tests/samples.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ describe("samples-generator", async () => {
const perform_search_Body = z
.object({
criteria: z.string().default("*:*"),
start: z.number().int().optional(),
start: z.number().int().optional().default(0),
rows: z.number().int().optional().default(100),
})
.passthrough();
Expand Down
Loading