From 8b710ff4cc37a957fcc8769aebdd76162e3f39b7 Mon Sep 17 00:00:00 2001 From: tankers746 Date: Mon, 18 Mar 2024 19:13:43 +0000 Subject: [PATCH 01/15] fix: explicit undefined check to not exclude falsy default values --- lib/src/openApiToZod.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/openApiToZod.ts b/lib/src/openApiToZod.ts index 7ff69db7..d2772f0f 100644 --- a/lib/src/openApiToZod.ts +++ b/lib/src/openApiToZod.ts @@ -341,7 +341,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)); From aa4c7a3668c6d96492bcd319ccd940f0b735b029 Mon Sep 17 00:00:00 2001 From: Thomas Ankers Date: Mon, 18 Mar 2024 19:20:26 +0000 Subject: [PATCH 02/15] Create rude-ways-shake.md --- .changeset/rude-ways-shake.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/rude-ways-shake.md diff --git a/.changeset/rude-ways-shake.md b/.changeset/rude-ways-shake.md new file mode 100644 index 00000000..44bafd97 --- /dev/null +++ b/.changeset/rude-ways-shake.md @@ -0,0 +1,5 @@ +--- +"openapi-zod-client": patch +--- + +Fixed bug which was excluding falsy default values From 9bc51647a7b1f362a2167fe5995678e381a13c09 Mon Sep 17 00:00:00 2001 From: Thomas Ankers Date: Mon, 18 Mar 2024 19:29:02 +0000 Subject: [PATCH 03/15] Update samples.test.ts --- lib/tests/samples.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tests/samples.test.ts b/lib/tests/samples.test.ts index 8c71c5b0..fb1f207a 100644 --- a/lib/tests/samples.test.ts +++ b/lib/tests/samples.test.ts @@ -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(); From 8d84b028ca561403cb869727c7ec0f6a186f5ec7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 26 Mar 2024 14:07:27 +0000 Subject: [PATCH 04/15] Version Packages --- .changeset/rude-ways-shake.md | 5 ----- lib/CHANGELOG.md | 6 ++++++ lib/package.json | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) delete mode 100644 .changeset/rude-ways-shake.md diff --git a/.changeset/rude-ways-shake.md b/.changeset/rude-ways-shake.md deleted file mode 100644 index 44bafd97..00000000 --- a/.changeset/rude-ways-shake.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"openapi-zod-client": patch ---- - -Fixed bug which was excluding falsy default values diff --git a/lib/CHANGELOG.md b/lib/CHANGELOG.md index dc098d92..2337ef45 100644 --- a/lib/CHANGELOG.md +++ b/lib/CHANGELOG.md @@ -1,5 +1,11 @@ # openapi-zod-client +## 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 diff --git a/lib/package.json b/lib/package.json index cdcc376c..6e32da6b 100644 --- a/lib/package.json +++ b/lib/package.json @@ -1,6 +1,6 @@ { "name": "openapi-zod-client", - "version": "1.16.2", + "version": "1.16.3", "repository": { "type": "git", "url": "https://github.com/astahmer/openapi-zod-client.git" From 7224486d79dbaef688a9e7a6bf37df9173eb7d8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Douglas=20Gad=C3=AAlha?= Date: Tue, 2 Apr 2024 15:21:57 -0300 Subject: [PATCH 05/15] fix: multiline description --- lib/src/getZodiosEndpointDefinitionList.ts | 2 +- lib/src/openApiToZod.ts | 6 +++- lib/tests/description-in-zod.test.ts | 32 ++++++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/lib/src/getZodiosEndpointDefinitionList.ts b/lib/src/getZodiosEndpointDefinitionList.ts index 40dbbe75..c572068f 100644 --- a/lib/src/getZodiosEndpointDefinitionList.ts +++ b/lib/src/getZodiosEndpointDefinitionList.ts @@ -249,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 diff --git a/lib/src/openApiToZod.ts b/lib/src/openApiToZod.ts index d2772f0f..88d505b6 100644 --- a/lib/src/openApiToZod.ts +++ b/lib/src/openApiToZod.ts @@ -302,7 +302,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 diff --git a/lib/tests/description-in-zod.test.ts b/lib/tests/description-in-zod.test.ts index 56344d03..31f3b400 100644 --- a/lib/tests/description-in-zod.test.ts +++ b/lib/tests/description-in-zod.test.ts @@ -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": { @@ -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(), }, From f3ee25efc191d0be97231498924fe50fd977fb88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Douglas=20Gad=C3=AAlha?= Date: Wed, 3 Apr 2024 11:56:24 -0300 Subject: [PATCH 06/15] feat: add changeset --- .changeset/chilly-needles-pump.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/chilly-needles-pump.md diff --git a/.changeset/chilly-needles-pump.md b/.changeset/chilly-needles-pump.md new file mode 100644 index 00000000..16660521 --- /dev/null +++ b/.changeset/chilly-needles-pump.md @@ -0,0 +1,5 @@ +--- +"openapi-zod-client": patch +--- + +Fix multiline descriptions when `describe` is enabled From c574b50c91e0135916e44f0368e564f204952898 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 3 Apr 2024 15:44:08 +0000 Subject: [PATCH 07/15] Version Packages --- .changeset/chilly-needles-pump.md | 5 ----- lib/CHANGELOG.md | 6 ++++++ lib/package.json | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) delete mode 100644 .changeset/chilly-needles-pump.md diff --git a/.changeset/chilly-needles-pump.md b/.changeset/chilly-needles-pump.md deleted file mode 100644 index 16660521..00000000 --- a/.changeset/chilly-needles-pump.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"openapi-zod-client": patch ---- - -Fix multiline descriptions when `describe` is enabled diff --git a/lib/CHANGELOG.md b/lib/CHANGELOG.md index 2337ef45..82ca7d97 100644 --- a/lib/CHANGELOG.md +++ b/lib/CHANGELOG.md @@ -1,5 +1,11 @@ # openapi-zod-client +## 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 diff --git a/lib/package.json b/lib/package.json index 6e32da6b..984b4557 100644 --- a/lib/package.json +++ b/lib/package.json @@ -1,6 +1,6 @@ { "name": "openapi-zod-client", - "version": "1.16.3", + "version": "1.16.4", "repository": { "type": "git", "url": "https://github.com/astahmer/openapi-zod-client.git" From e812354e578af2dde19b96b2f169e928be129b90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Douglas=20Gad=C3=AAlha?= Date: Wed, 10 Apr 2024 15:48:58 -0300 Subject: [PATCH 08/15] feat: schema refiner --- lib/src/openApiToZod.ts | 6 +++-- lib/src/template-context.ts | 8 ++++++- lib/tests/schema-refiner.test.ts | 41 ++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 lib/tests/schema-refiner.test.ts diff --git a/lib/src/openApiToZod.ts b/lib/src/openApiToZod.ts index 88d505b6..2bd35d25 100644 --- a/lib/src/openApiToZod.ts +++ b/lib/src/openApiToZod.ts @@ -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), diff --git a/lib/src/template-context.ts b/lib/src/template-context.ts index 03f34d14..aced2cbd 100644 --- a/lib/src/template-context.ts +++ b/lib/src/template-context.ts @@ -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"; @@ -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 }); @@ -403,4 +404,9 @@ export type TemplateContextOptions = { * 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?: (schema: T, parentMeta?: CodeMetaData) => T; }; diff --git a/lib/tests/schema-refiner.test.ts b/lib/tests/schema-refiner.test.ts new file mode 100644 index 00000000..79b89b42 --- /dev/null +++ b/lib/tests/schema-refiner.test.ts @@ -0,0 +1,41 @@ +import { isReferenceObject } from "openapi3-ts"; +import { getZodSchema } from "../src/openApiToZod"; +import { test, expect } from "vitest"; + +test("schema-refiner", () => { + expect( + getZodSchema({ + schema: { + properties: { + name: { + type: "string", + }, + email: { + type: "string", + }, + }, + }, + options: { + schemaRefiner(schema) { + if (isReferenceObject(schema) || !schema.properties) { + return schema; + } + + if (!schema.required && schema.properties) { + for (const key in schema.properties) { + const prop = schema.properties[key]; + + if (!isReferenceObject(prop)) { + prop.nullable = true; + } + } + } + + return schema; + }, + }, + }) + ).toMatchInlineSnapshot( + '"z.object({ name: z.string().nullable(), email: z.string().nullable() }).partial().passthrough()"' + ); +}); From 3ec491572e56fc40e3b49cefb58cb6f08600190f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Douglas=20Gad=C3=AAlha?= Date: Wed, 10 Apr 2024 15:53:46 -0300 Subject: [PATCH 09/15] feat: add changeset according to SEMVER spec, this must be a minor update: > Minor version Y (x.Y.z | x > 0) MUST be incremented if new, backward compatible functionality is introduced to the public API. --- .changeset/ten-poems-appear.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/ten-poems-appear.md diff --git a/.changeset/ten-poems-appear.md b/.changeset/ten-poems-appear.md new file mode 100644 index 00000000..6e08c318 --- /dev/null +++ b/.changeset/ten-poems-appear.md @@ -0,0 +1,5 @@ +--- +"openapi-zod-client": minor +--- + +Add `schemaRefiner` option to allow refining the OpenAPI schema before its converted to a Zod schema From 494583f94ac5ed4464053ea7ccdb14c86a19544a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 11 Apr 2024 07:35:48 +0000 Subject: [PATCH 10/15] Version Packages --- .changeset/ten-poems-appear.md | 5 ----- lib/CHANGELOG.md | 6 ++++++ lib/package.json | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) delete mode 100644 .changeset/ten-poems-appear.md diff --git a/.changeset/ten-poems-appear.md b/.changeset/ten-poems-appear.md deleted file mode 100644 index 6e08c318..00000000 --- a/.changeset/ten-poems-appear.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"openapi-zod-client": minor ---- - -Add `schemaRefiner` option to allow refining the OpenAPI schema before its converted to a Zod schema diff --git a/lib/CHANGELOG.md b/lib/CHANGELOG.md index 82ca7d97..0a6cf0be 100644 --- a/lib/CHANGELOG.md +++ b/lib/CHANGELOG.md @@ -1,5 +1,11 @@ # 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 diff --git a/lib/package.json b/lib/package.json index 984b4557..d75a1e31 100644 --- a/lib/package.json +++ b/lib/package.json @@ -1,6 +1,6 @@ { "name": "openapi-zod-client", - "version": "1.16.4", + "version": "1.17.0", "repository": { "type": "git", "url": "https://github.com/astahmer/openapi-zod-client.git" From 1d398b4235be6c6cc17e27f9f733ba0f1799b696 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20S=C3=A9n=C3=A9cal?= Date: Sat, 16 Mar 2024 15:44:00 +0100 Subject: [PATCH 11/15] feat: Add JSDoc comments to generated types with `--with-docs` CLI option --- README.md | 1 + lib/src/cli.ts | 2 ++ lib/src/generateJSDocArray.ts | 44 ++++++++++++++++++++++++++++++++++ lib/src/openApiToTypescript.ts | 26 ++++++++++++++++---- lib/src/template-context.ts | 5 ++++ 5 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 lib/src/generateJSDocArray.ts diff --git a/README.md b/README.md index 912400fe..7a5169c6 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ Options: --implicit-required When true, will make all properties of an object required by default (rather than the current opposite), unless an explicitly `required` array is set --with-deprecated when true, will keep deprecated endpoints in the api output --with-description when true, will add z.describe(xxx) + --with-docs when true, will add jsdoc comments to generated types --group-strategy groups endpoints by a given strategy, possible values are: 'none' | 'tag' | 'method' | 'tag-file' | 'method-file' --complexity-threshold schema complexity threshold to determine which one (using less than `<` operator) should be assigned to a variable --default-status when defined as `auto-correct`, will automatically use `default` as fallback for `response` when no status code was declared diff --git a/lib/src/cli.ts b/lib/src/cli.ts index c0f8c7a2..206ed2a0 100644 --- a/lib/src/cli.ts +++ b/lib/src/cli.ts @@ -37,6 +37,7 @@ cli.command("", "path/url to OpenAPI/Swagger document as json/yaml") ) .option("--with-deprecated", "when true, will keep deprecated endpoints in the api output") .option("--with-description", "when true, will add z.describe(xxx)") + .option("--with-docs", "when true, will add jsdoc comments to generated types") .option( "--group-strategy", "groups endpoints by a given strategy, possible values are: 'none' | 'tag' | 'method' | 'tag-file' | 'method-file'" @@ -85,6 +86,7 @@ cli.command("", "path/url to OpenAPI/Swagger document as json/yaml") isMediaTypeAllowed: options.mediaTypeExpr, withImplicitRequiredProps: options.implicitRequired, withDeprecatedEndpoints: options.withDeprecated, + withDocs: options.withDocs, groupStrategy: options.groupStrategy, complexityThreshold: options.complexityThreshold, defaultStatusBehavior: options.defaultStatus, diff --git a/lib/src/generateJSDocArray.ts b/lib/src/generateJSDocArray.ts new file mode 100644 index 00000000..257b6282 --- /dev/null +++ b/lib/src/generateJSDocArray.ts @@ -0,0 +1,44 @@ +import type { SchemaObject } from "openapi3-ts"; + +export default function generateJSDocArray(schema: SchemaObject, withTypesAndFormat = false): string[] { + const comments: string[] = []; + + const mapping = { + description: (value: string) => `${value}`, + example: (value: any) => `@example ${JSON.stringify(value)}`, + examples: (value: any[]) => + value.map((example, index) => `@example Example ${index + 1}: ${JSON.stringify(example)}`).join("\n"), + deprecated: (value: boolean) => (value ? "@deprecated" : ""), + externalDocs: (value: { url: string }) => `@see ${value.url}`, + // Additional attributes that depend on `withTypesAndFormat` + type: withTypesAndFormat + ? (value: string | string[]) => `@type {${Array.isArray(value) ? value.join("|") : value}}` + : undefined, + format: withTypesAndFormat ? (value: string) => `@format ${value}` : undefined, + minimum: (value: number) => `@minimum ${value}`, + maximum: (value: number) => `@maximum ${value}`, + minLength: (value: number) => `@minLength ${value}`, + maxLength: (value: number) => `@maxLength ${value}`, + pattern: (value: string) => `@pattern ${value}`, + enum: (value: string[]) => `@enum ${value.join(", ")}`, + }; + + Object.entries(mapping).forEach(([key, mappingFunction]) => { + const schemaValue = schema[key as keyof SchemaObject]; + if (schemaValue !== undefined && mappingFunction) { + const result = mappingFunction(schemaValue); + if (Array.isArray(result)) { + result.forEach((subResult) => comments.push(subResult)); + } else if (result) { + comments.push(result); + } + } + }); + + // Add a space line after description if there are other comments + if (comments.length > 1 && !!schema.description) { + comments.splice(1, 0, ""); + } + + return comments; +} diff --git a/lib/src/openApiToTypescript.ts b/lib/src/openApiToTypescript.ts index bd945fd2..cb7a09b0 100644 --- a/lib/src/openApiToTypescript.ts +++ b/lib/src/openApiToTypescript.ts @@ -7,6 +7,7 @@ import type { DocumentResolver } from "./makeSchemaResolver"; import type { TemplateContext } from "./template-context"; import { wrapWithQuotesIfNeeded } from "./utils"; import { inferRequiredSchema } from "./inferRequiredOnly"; +import generateJSDocArray from "./generateJSDocArray"; type TsConversionArgs = { schema: SchemaObject | ReferenceObject; @@ -155,6 +156,7 @@ TsConversionArgs): ts.Node | TypeDefinitionObject | string => { if (schema.allOf.length === 1) { return getTypescriptFromOpenApi({ schema: schema.allOf[0]!, ctx, meta, options }); } + const { patchRequiredSchemaInLoop, noRequiredOnlyAllof, composedRequiredSchema } = inferRequiredSchema(schema); @@ -164,7 +166,7 @@ TsConversionArgs): ts.Node | TypeDefinitionObject | string => { return type; }); - if (Object.keys(composedRequiredSchema.properties).length) { + if (Object.keys(composedRequiredSchema.properties).length > 0) { types.push( getTypescriptFromOpenApi({ schema: composedRequiredSchema, @@ -174,6 +176,7 @@ TsConversionArgs): ts.Node | TypeDefinitionObject | string => { }) as TypeDefinition ); } + return schema.nullable ? t.union([t.intersection(types), t.reference("null")]) : t.intersection(types); } @@ -294,8 +297,9 @@ TsConversionArgs): ts.Node | TypeDefinitionObject | string => { throw new Error("Name is required to convert an object schema to a type reference"); } - const base = t.type(inheritedMeta.name, doWrapReadOnly(objectType)); - if (!isPartial) return base; + if (!isPartial) { + return t.type(inheritedMeta.name, doWrapReadOnly(objectType)); + } return t.type(inheritedMeta.name, t.reference("Partial", [doWrapReadOnly(objectType)])); } @@ -305,7 +309,21 @@ TsConversionArgs): ts.Node | TypeDefinitionObject | string => { throw new Error(`Unsupported schema type: ${schemaType}`); }; - const tsResult = getTs(); + let tsResult = getTs(); + + // Add JSDoc comments + if (options?.withDocs && !isReferenceObject(schema)) { + const jsDocComments = generateJSDocArray(schema); + + if ( + jsDocComments.length > 0 && + typeof tsResult === "object" && + tsResult.kind !== ts.SyntaxKind.TypeAliasDeclaration + ) { + tsResult = t.comment(tsResult, jsDocComments); + } + } + return canBeWrapped ? wrapTypeIfInline({ isInline, name: inheritedMeta?.name, typeDef: tsResult as TypeDefinition }) : tsResult; diff --git a/lib/src/template-context.ts b/lib/src/template-context.ts index aced2cbd..48a61ea7 100644 --- a/lib/src/template-context.ts +++ b/lib/src/template-context.ts @@ -335,6 +335,11 @@ export type TemplateContextOptions = { * @default false */ withDeprecatedEndpoints?: boolean; + /** + * when true, will add jsdoc comments to generated types + * @default false + */ + withDocs?: boolean; /** * groups endpoints by a given strategy * From 323a34e08fc33627e6eac8804454a9d665f4f8dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Douglas=20Gad=C3=AAlha?= Date: Sat, 13 Apr 2024 18:07:09 -0300 Subject: [PATCH 12/15] feat: add jsdoc tests --- lib/src/generateJSDocArray.ts | 3 +- lib/tests/jsdoc.test.ts | 202 ++++++++++++++++++++++++++++++++++ 2 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 lib/tests/jsdoc.test.ts diff --git a/lib/src/generateJSDocArray.ts b/lib/src/generateJSDocArray.ts index 257b6282..60ed63e8 100644 --- a/lib/src/generateJSDocArray.ts +++ b/lib/src/generateJSDocArray.ts @@ -7,8 +7,9 @@ export default function generateJSDocArray(schema: SchemaObject, withTypesAndFor description: (value: string) => `${value}`, example: (value: any) => `@example ${JSON.stringify(value)}`, examples: (value: any[]) => - value.map((example, index) => `@example Example ${index + 1}: ${JSON.stringify(example)}`).join("\n"), + value.map((example, index) => `@example Example ${index + 1}: ${JSON.stringify(example)}`), deprecated: (value: boolean) => (value ? "@deprecated" : ""), + default: (value: any) => `@default ${JSON.stringify(value)}`, externalDocs: (value: { url: string }) => `@see ${value.url}`, // Additional attributes that depend on `withTypesAndFormat` type: withTypesAndFormat diff --git a/lib/tests/jsdoc.test.ts b/lib/tests/jsdoc.test.ts new file mode 100644 index 00000000..ba91812e --- /dev/null +++ b/lib/tests/jsdoc.test.ts @@ -0,0 +1,202 @@ +import { OpenAPIObject } from "openapi3-ts"; +import { test, expect } from "vitest"; +import { generateZodClientFromOpenAPI } from "../src"; + +test("jsdoc", async () => { + const openApiDoc: OpenAPIObject = { + openapi: "3.0.3", + info: { version: "1", title: "Example API" }, + paths: { + "/test": { + get: { + operationId: "123_example", + responses: { + "200": { + content: { "application/json": { schema: { $ref: "#/components/schemas/ComplexObject" } } }, + }, + }, + }, + }, + }, + components: { + schemas: { + SimpleObject: { + type: "object", + properties: { + str: { type: "string" }, + }, + }, + ComplexObject: { + type: "object", + properties: { + example: { + type: "string", + description: "A string with example tag", + example: "example", + }, + examples: { + type: "string", + description: "A string with examples tag", + examples: ["example1", "example2"], + }, + manyTagsStr: { + type: "string", + description: "A string with many tags", + minLength: 1, + maxLength: 10, + pattern: "^[a-z]*$", + enum: ["a", "b", "c"], + }, + numMin: { + type: "number", + description: "A number with minimum tag", + minimum: 0, + }, + numMax: { + type: "number", + description: "A number with maximum tag", + maximum: 10, + }, + manyTagsNum: { + type: "number", + description: "A number with many tags", + minimum: 0, + maximum: 10, + default: 5, + example: 3, + deprecated: true, + externalDocs: { url: "https://example.com" }, + }, + bool: { + type: "boolean", + description: "A boolean", + default: true, + }, + ref: { $ref: "#/components/schemas/SimpleObject" }, + refArray: { + type: "array", + description: "An array of SimpleObject", + items: { + $ref: "#/components/schemas/SimpleObject", + }, + }, + }, + }, + }, + }, + }; + + const output = await generateZodClientFromOpenAPI({ + disableWriteToFile: true, + openApiDoc, + options: { + withDocs: true, + shouldExportAllTypes: true, + }, + }); + + expect(output).toMatchInlineSnapshot(`"import { makeApi, Zodios, type ZodiosOptions } from "@zodios/core"; +import { z } from "zod"; + +type ComplexObject = Partial<{ + /** + * A string with example tag + * + * @example "example" + */ + example: string; + /** + * A string with examples tag + * + * @example Example 1: "example1" + * @example Example 2: "example2" + */ + examples: string; + /** + * A string with many tags + * + * @minLength 1 + * @maxLength 10 + * @pattern ^[a-z]*$ + * @enum a, b, c + */ + manyTagsStr: "a" | "b" | "c"; + /** + * A number with minimum tag + * + * @minimum 0 + */ + numMin: number; + /** + * A number with maximum tag + * + * @maximum 10 + */ + numMax: number; + /** + * A number with many tags + * + * @example 3 + * @deprecated + * @default 5 + * @see https://example.com + * @minimum 0 + * @maximum 10 + */ + manyTagsNum: number; + /** + * A boolean + * + * @default true + */ + bool: boolean; + ref: SimpleObject; + /** + * An array of SimpleObject + */ + refArray: Array; +}>; +type SimpleObject = Partial<{ + str: string; +}>; + +const SimpleObject: z.ZodType = z + .object({ str: z.string() }) + .partial() + .passthrough(); +const ComplexObject: z.ZodType = z + .object({ + example: z.string(), + examples: z.string(), + manyTagsStr: z.enum(["a", "b", "c"]).regex(/^[a-z]*$/), + numMin: z.number().gte(0), + numMax: z.number().lte(10), + manyTagsNum: z.number().gte(0).lte(10).default(5), + bool: z.boolean().default(true), + ref: SimpleObject, + refArray: z.array(SimpleObject), + }) + .partial() + .passthrough(); + +export const schemas = { + SimpleObject, + ComplexObject, +}; + +const endpoints = makeApi([ + { + method: "get", + path: "/test", + requestFormat: "json", + response: ComplexObject, + }, +]); + +export const api = new Zodios(endpoints); + +export function createApiClient(baseUrl: string, options?: ZodiosOptions) { + return new Zodios(baseUrl, endpoints, options); +} +"`); +}); From ed500762c6998fb2976e8ad43a88a3a09d928f2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Douglas=20Gad=C3=AAlha?= Date: Sat, 13 Apr 2024 18:11:40 -0300 Subject: [PATCH 13/15] feat: add changeset --- .changeset/shy-rocks-fix.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/shy-rocks-fix.md diff --git a/.changeset/shy-rocks-fix.md b/.changeset/shy-rocks-fix.md new file mode 100644 index 00000000..70ec1d87 --- /dev/null +++ b/.changeset/shy-rocks-fix.md @@ -0,0 +1,5 @@ +--- +"openapi-zod-client": minor +--- + +Add `withDocs` option and `--with-docs` flag that adds JSDoc to generated code From c99e56ec967df8420a76269db6a5bd70e997e60f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 13 Apr 2024 21:17:06 +0000 Subject: [PATCH 14/15] Version Packages --- .changeset/shy-rocks-fix.md | 5 ----- lib/CHANGELOG.md | 6 ++++++ lib/package.json | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) delete mode 100644 .changeset/shy-rocks-fix.md diff --git a/.changeset/shy-rocks-fix.md b/.changeset/shy-rocks-fix.md deleted file mode 100644 index 70ec1d87..00000000 --- a/.changeset/shy-rocks-fix.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"openapi-zod-client": minor ---- - -Add `withDocs` option and `--with-docs` flag that adds JSDoc to generated code diff --git a/lib/CHANGELOG.md b/lib/CHANGELOG.md index 0a6cf0be..ffcd2302 100644 --- a/lib/CHANGELOG.md +++ b/lib/CHANGELOG.md @@ -1,5 +1,11 @@ # openapi-zod-client +## 1.18.0 + +### Minor Changes + +- [#275](https://github.com/astahmer/openapi-zod-client/pull/275) [`ed50076`](https://github.com/astahmer/openapi-zod-client/commit/ed500762c6998fb2976e8ad43a88a3a09d928f2c) Thanks [@senecolas](https://github.com/senecolas)! - Add `withDocs` option and `--with-docs` flag that adds JSDoc to generated code + ## 1.17.0 ### Minor Changes diff --git a/lib/package.json b/lib/package.json index d75a1e31..1f981cfb 100644 --- a/lib/package.json +++ b/lib/package.json @@ -1,6 +1,6 @@ { "name": "openapi-zod-client", - "version": "1.17.0", + "version": "1.18.0", "repository": { "type": "git", "url": "https://github.com/astahmer/openapi-zod-client.git" From 6ee8b2f4cbe13bb362b5e67ade4d654919a69226 Mon Sep 17 00:00:00 2001 From: Mike Perrone Date: Tue, 16 Apr 2024 15:37:40 -0700 Subject: [PATCH 15/15] correct test expectation to look for a backslash --- lib/tests/regex-with-escapes.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/tests/regex-with-escapes.test.ts b/lib/tests/regex-with-escapes.test.ts index 69c75fc7..4278c296 100644 --- a/lib/tests/regex-with-escapes.test.ts +++ b/lib/tests/regex-with-escapes.test.ts @@ -8,11 +8,11 @@ test("regex-with-escapes", () => { properties: { str: { type: "string", - pattern: "^\/$" + pattern: "^/$" }, } }}) ).toMatchInlineSnapshot( - '"z.object({ str: z.string().regex(/^\/$/) }).partial().passthrough()"' + '"z.object({ str: z.string().regex(/^\\/$/) }).partial().passthrough()"' ); });