From 0cb3c0b6cdb9e2dd8a8c7bb894d924c43d149f77 Mon Sep 17 00:00:00 2001 From: Alexander Galabov Date: Tue, 17 Oct 2023 14:33:58 +0300 Subject: [PATCH 1/2] #176 added support for bigint --- README.md | 1 + spec/types/bigint.spec.ts | 92 +++++++++++++++++++++++++++++++++++++++ src/lib/zod-is-type.ts | 1 + src/openapi-generator.ts | 15 +++++-- src/types.ts | 3 ++ src/v3.0/specifics.ts | 12 ++--- src/v3.1/specifics.ts | 12 ++--- 7 files changed, 121 insertions(+), 15 deletions(-) create mode 100644 spec/types/bigint.spec.ts create mode 100644 src/types.ts diff --git a/README.md b/README.md index 7131d10..eb1921c 100644 --- a/README.md +++ b/README.md @@ -492,6 +492,7 @@ The list of all supported types as of now is: - `ZodAny` - `ZodArray` +- `ZodBigInt` - `ZodBoolean` - `ZodDate` - `ZodDefault` diff --git a/spec/types/bigint.spec.ts b/spec/types/bigint.spec.ts new file mode 100644 index 0000000..ce4e435 --- /dev/null +++ b/spec/types/bigint.spec.ts @@ -0,0 +1,92 @@ +import { z } from 'zod'; +import { expectSchema } from '../lib/helpers'; + +describe('bigint', () => { + it('generates OpenAPI schema for a simple bigint type', () => { + expectSchema([z.bigint().openapi('SimpleBigInt')], { + SimpleBigInt: { type: 'integer', format: 'int64' }, + }); + }); + + it('supports minimum in open api 3.0.0', () => { + expectSchema([z.bigint().gte(BigInt(0)).openapi('SimpleBigInteger')], { + SimpleBigInteger: { type: 'integer', format: 'int64', minimum: 0 }, + }); + }); + + it('supports exclusive minimum in open api 3.0.0', () => { + expectSchema([z.bigint().gt(BigInt(0)).openapi('SimpleBigInteger')], { + SimpleBigInteger: { + type: 'integer', + format: 'int64', + minimum: 0, + exclusiveMinimum: true, + }, + }); + }); + + it('supports maximum in open api 3.0.0', () => { + expectSchema([z.bigint().lte(BigInt(0)).openapi('SimpleBigInteger')], { + SimpleBigInteger: { type: 'integer', format: 'int64', maximum: 0 }, + }); + }); + + it('supports exclusive maximum in open api 3.0.0', () => { + expectSchema([z.bigint().lt(BigInt(0)).openapi('SimpleBigInteger')], { + SimpleBigInteger: { + type: 'integer', + format: 'int64', + maximum: 0, + exclusiveMaximum: true, + }, + }); + }); + + it('supports minimum in open api 3.1.0', () => { + expectSchema( + [z.bigint().gte(BigInt(0)).openapi('SimpleBigInteger')], + { + SimpleBigInteger: { type: 'integer', format: 'int64', minimum: 0 }, + }, + '3.1.0' + ); + }); + + it('supports exclusive minimum in open api 3.1.0', () => { + expectSchema( + [z.bigint().gt(BigInt(0)).openapi('SimpleBigInteger')], + { + SimpleBigInteger: { + type: 'integer', + format: 'int64', + exclusiveMinimum: 0, + } as never, + }, + '3.1.0' + ); + }); + + it('supports maximum in open api 3.1.0', () => { + expectSchema( + [z.bigint().lte(BigInt(0)).openapi('SimpleBigInteger')], + { + SimpleBigInteger: { type: 'integer', format: 'int64', maximum: 0 }, + }, + '3.1.0' + ); + }); + + it('supports exclusive maximum in open api 3.1.0', () => { + expectSchema( + [z.bigint().lt(BigInt(0)).openapi('SimpleBigInteger')], + { + SimpleBigInteger: { + type: 'integer', + format: 'int64', + exclusiveMaximum: 0, + } as never, + }, + '3.1.0' + ); + }); +}); diff --git a/src/lib/zod-is-type.ts b/src/lib/zod-is-type.ts index 4d0591f..1b74d39 100644 --- a/src/lib/zod-is-type.ts +++ b/src/lib/zod-is-type.ts @@ -3,6 +3,7 @@ import type { z } from 'zod'; type ZodTypes = { ZodAny: z.ZodAny; ZodArray: z.ZodArray; + ZodBigInt: z.ZodBigInt; ZodBoolean: z.ZodBoolean; ZodBranded: z.ZodBranded; ZodDefault: z.ZodDefault; diff --git a/src/openapi-generator.ts b/src/openapi-generator.ts index 69791ff..964768d 100644 --- a/src/openapi-generator.ts +++ b/src/openapi-generator.ts @@ -45,7 +45,6 @@ type HeadersObject = HeadersObject30 & HeadersObject31; import type { AnyZodObject, - ZodNumberDef, ZodObject, ZodRawShape, ZodString, @@ -80,6 +79,7 @@ import { ZodRequestBody, } from './openapi-registry'; import { ZodOpenApiFullMetadata, ZodOpenAPIMetadata } from './zod-extensions'; +import { ZodNumericCheck } from './types'; // See https://github.com/colinhacks/zod/blob/9eb7eb136f3e702e86f030e6984ef20d4d8521b6/src/types.ts#L1370 type UnknownKeysParam = 'passthrough' | 'strict' | 'strip'; @@ -104,7 +104,7 @@ export interface OpenApiVersionSpecifics { isNullable: boolean ): Pick; - getNumberChecks(checks: ZodNumberDef['checks']): any; + getNumberChecks(checks: ZodNumericCheck[]): any; } export class OpenAPIGenerator { @@ -760,7 +760,7 @@ export class OpenAPIGenerator { } private getNumberChecks( - checks: ZodNumberDef['checks'] + checks: ZodNumericCheck[] ): Pick< SchemaObject, 'minimum' | 'exclusiveMinimum' | 'maximum' | 'exclusiveMaximum' @@ -828,6 +828,15 @@ export class OpenAPIGenerator { }; } + if (isZodType(zodSchema, 'ZodBigInt')) { + return { + ...this.mapNullableType('integer', isNullable), + ...this.getNumberChecks(zodSchema._def.checks), + format: 'int64', + default: defaultValue, + }; + } + if (isZodType(zodSchema, 'ZodBoolean')) { return { ...this.mapNullableType('boolean', isNullable), diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..cb121b3 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,3 @@ +import { ZodBigIntCheck, ZodNumberCheck } from 'zod'; + +export type ZodNumericCheck = ZodNumberCheck | ZodBigIntCheck; diff --git a/src/v3.0/specifics.ts b/src/v3.0/specifics.ts index a78fb23..6622b8b 100644 --- a/src/v3.0/specifics.ts +++ b/src/v3.0/specifics.ts @@ -1,6 +1,6 @@ import type { ReferenceObject, SchemaObject } from 'openapi3-ts/oas30'; -import { ZodNumberDef } from 'zod'; import { OpenApiVersionSpecifics } from '../openapi-generator'; +import { ZodNumericCheck } from '../types'; export class OpenApiGeneratorV30Specifics implements OpenApiVersionSpecifics { get nullType() { @@ -28,7 +28,7 @@ export class OpenApiGeneratorV30Specifics implements OpenApiVersionSpecifics { } getNumberChecks( - checks: ZodNumberDef['checks'] + checks: ZodNumericCheck[] ): Pick< SchemaObject, 'minimum' | 'exclusiveMinimum' | 'maximum' | 'exclusiveMaximum' @@ -39,13 +39,13 @@ export class OpenApiGeneratorV30Specifics implements OpenApiVersionSpecifics { switch (check.kind) { case 'min': return check.inclusive - ? { minimum: check.value } - : { minimum: check.value, exclusiveMinimum: true }; + ? { minimum: Number(check.value) } + : { minimum: Number(check.value), exclusiveMinimum: true }; case 'max': return check.inclusive - ? { maximum: check.value } - : { maximum: check.value, exclusiveMaximum: true }; + ? { maximum: Number(check.value) } + : { maximum: Number(check.value), exclusiveMaximum: true }; default: return {}; diff --git a/src/v3.1/specifics.ts b/src/v3.1/specifics.ts index 86919b8..3457fbe 100644 --- a/src/v3.1/specifics.ts +++ b/src/v3.1/specifics.ts @@ -1,7 +1,7 @@ import type { ReferenceObject, SchemaObject } from 'openapi3-ts/oas31'; -import type { ZodNumberDef } from 'zod'; import { OpenApiVersionSpecifics } from '../openapi-generator'; +import { ZodNumericCheck } from '../types'; export class OpenApiGeneratorV31Specifics implements OpenApiVersionSpecifics { get nullType() { @@ -40,7 +40,7 @@ export class OpenApiGeneratorV31Specifics implements OpenApiVersionSpecifics { } getNumberChecks( - checks: ZodNumberDef['checks'] + checks: ZodNumericCheck[] ): Pick< SchemaObject, 'minimum' | 'exclusiveMinimum' | 'maximum' | 'exclusiveMaximum' @@ -51,13 +51,13 @@ export class OpenApiGeneratorV31Specifics implements OpenApiVersionSpecifics { switch (check.kind) { case 'min': return check.inclusive - ? { minimum: check.value } - : { exclusiveMinimum: check.value }; + ? { minimum: Number(check.value) } + : { exclusiveMinimum: Number(check.value) }; case 'max': return check.inclusive - ? { maximum: check.value } - : { exclusiveMaximum: check.value }; + ? { maximum: Number(check.value) } + : { exclusiveMaximum: Number(check.value) }; default: return {}; From 890b45a6bf65281a9de08e9e99e7baa2af3310a1 Mon Sep 17 00:00:00 2001 From: Alexander Galabov Date: Tue, 17 Oct 2023 14:39:33 +0300 Subject: [PATCH 2/2] #177 fix .describe for paramteres not putting the description in the correct place --- spec/modifiers/describe.spec.ts | 76 ++++++++++++++++++++++++++++++++- src/openapi-generator.ts | 33 +++++++++++++- 2 files changed, 107 insertions(+), 2 deletions(-) diff --git a/spec/modifiers/describe.spec.ts b/spec/modifiers/describe.spec.ts index 7bb448a..34e2dc5 100644 --- a/spec/modifiers/describe.spec.ts +++ b/spec/modifiers/describe.spec.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { expectSchema } from '../lib/helpers'; +import { expectSchema, generateDataForRoute } from '../lib/helpers'; describe('describe', () => { it('generates OpenAPI schema with description when the .describe method is used', () => { @@ -78,4 +78,78 @@ describe('describe', () => { }, }); }); + + it('generates an optional query parameter with a provided description', () => { + const { parameters } = generateDataForRoute({ + request: { + query: z.object({ + test: z.string().optional().describe('Some parameter'), + }), + }, + }); + + expect(parameters).toEqual([ + { + in: 'query', + name: 'test', + description: 'Some parameter', + required: false, + schema: { + description: 'Some parameter', + type: 'string', + }, + }, + ]); + }); + + it('generates a query parameter with a description made optional', () => { + const { parameters } = generateDataForRoute({ + request: { + query: z.object({ + test: z.string().describe('Some parameter').optional(), + }), + }, + }); + + expect(parameters).toEqual([ + { + in: 'query', + name: 'test', + description: 'Some parameter', + required: false, + schema: { + description: 'Some parameter', + type: 'string', + }, + }, + ]); + }); + + it('generates a query parameter with description from a registered schema', () => { + const schema = z.string().describe('Some parameter').openapi('SomeString'); + const { parameters, documentSchemas } = generateDataForRoute({ + request: { + query: z.object({ test: schema }), + }, + }); + + expect(documentSchemas).toEqual({ + SomeString: { + type: 'string', + description: 'Some parameter', + }, + }); + + expect(parameters).toEqual([ + { + in: 'query', + name: 'test', + description: 'Some parameter', + required: true, + schema: { + $ref: '#/components/schemas/SomeString', + }, + }, + ]); + }); }); diff --git a/src/openapi-generator.ts b/src/openapi-generator.ts index 964768d..5af5dc8 100644 --- a/src/openapi-generator.ts +++ b/src/openapi-generator.ts @@ -359,7 +359,7 @@ export class OpenAPIGenerator { } private generateSimpleParameter(zodSchema: ZodTypeAny): BaseParameterObject { - const metadata = this.getMetadata(zodSchema); + const metadata = this.getParamMetadata(zodSchema); const paramMetadata = metadata?.metadata?.param; const required = @@ -1222,6 +1222,37 @@ export class OpenAPIGenerator { return omitBy(metadata, isNil); } + private getParamMetadata( + zodSchema: ZodType + ): ZodOpenApiFullMetadata | undefined { + const innerSchema = this.unwrapChained(zodSchema); + + const metadata = zodSchema._def.openapi + ? zodSchema._def.openapi + : innerSchema._def.openapi; + + /** + * Every zod schema can receive a `description` by using the .describe method. + * That description should be used when generating an OpenApi schema. + * The `??` bellow makes sure we can handle both: + * - schema.describe('Test').optional() + * - schema.optional().describe('Test') + */ + const zodDescription = zodSchema.description ?? innerSchema.description; + + return { + _internal: metadata?._internal, + metadata: { + ...metadata?.metadata, + // A description provided from .openapi() should be taken with higher precedence + param: { + description: zodDescription, + ...metadata?.metadata.param, + }, + }, + }; + } + private getMetadata( zodSchema: ZodType ): ZodOpenApiFullMetadata | undefined {